1use std::{
2 io::{self, Stdout, Write},
3 rc::Rc,
4};
5
6use anyhow::{Context, Result};
7use crossterm::{
8 execute,
9 terminal::{disable_raw_mode, LeaveAlternateScreen},
10};
11use nucleo::Config;
12use parking_lot::MutexGuard;
13use ratatui::{
14 backend::CrosstermBackend,
15 layout::{Constraint, Direction, Layout, Offset, Position, Rect, Size},
16 prelude::*,
17 style::{Color, Modifier, Style},
18 text::{Line, Span},
19 widgets::{Block, Borders, Paragraph},
20 Frame, Terminal,
21};
22
23use crate::io::{read_last_log_line, DrawMenu, ImageAdapter};
24use crate::log_info;
25use crate::modes::{
26 highlighted_text, parse_input_permission, AnsiString, BinLine, BinaryContent, Content,
27 ContentWindow, CursorOffset, Display as DisplayMode, DisplayedImage, FileInfo, FuzzyFinder,
28 HLContent, Input, InputSimple, LineDisplay, Menu as MenuMode, MoreInfos, Navigate,
29 NeedConfirmation, Preview, Remote, SecondLine, Selectable, TLine, TakeSkip, TakeSkipEnum, Text,
30 TextKind, Trash, Tree,
31};
32use crate::{
33 app::{ClickableLine, Footer, Header, PreviewHeader, Status, Tab},
34 modes::Users,
35};
36use crate::{
37 colored_skip_take,
38 config::{with_icon, with_icon_metadata, ColorG, Gradient, MATCHER, MENU_STYLES},
39};
40use crate::{common::path_to_string, modes::Icon};
41
42use super::ImageDisplayer;
43
44pub trait Offseted {
45 fn offseted(&self, x: u16, y: u16) -> Self;
46}
47
48impl Offseted for Rect {
49 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(2, 0);
471 let formater = Self::pick_formater(self.status.session.metadata(), p_rect.width);
472 let with_icon = with_icon();
473 let lines: Vec<_> = self
474 .tab
475 .dir_enum_skip_take()
476 .map(|(index, file)| {
477 self.files_line(group_owner_sizes, index, file, &formater, with_icon)
478 })
479 .collect();
480 Paragraph::new(lines).render(p_rect, f.buffer_mut());
481 }
482
483 fn pick_formater(with_metadata: bool, width: u16) -> Formater {
484 let kind = FormatKind::from_flags(with_metadata, width);
485
486 match kind {
487 FormatKind::Metadata => FileFormater::metadata,
488 FormatKind::MetadataNoGroup => FileFormater::metadata_no_group,
489 FormatKind::MetadataNoPermissions => FileFormater::metadata_no_permissions,
490 FormatKind::MetadataNoOwner => FileFormater::metadata_no_owner,
491 FormatKind::Simple => FileFormater::simple,
492 }
493 }
494
495 fn group_owner_size(&self) -> (usize, usize) {
496 if self.status.session.metadata() {
497 (
498 self.tab.directory.group_column_width(),
499 self.tab.directory.owner_column_width(),
500 )
501 } else {
502 (0, 0)
503 }
504 }
505
506 fn files_line<'b>(
507 &self,
508 group_owner_sizes: (usize, usize),
509 index: usize,
510 file: &FileInfo,
511 formater: &fn(&FileInfo, (usize, usize)) -> String,
512 with_icon: bool,
513 ) -> Line<'b> {
514 let mut style = file.style();
515 self.reverse_selected(index, &mut style);
516 self.color_searched(file, &mut style);
517 let mut content = formater(file, group_owner_sizes);
518
519 content.push(' ');
520 if with_icon {
521 content.push_str(file.icon());
522 }
523 content.push_str(&file.filename);
524
525 Line::from(vec![
526 self.span_flagged_symbol(file, &mut style),
527 Span::styled(content, style),
528 ])
529 }
530
531 fn reverse_selected(&self, index: usize, style: &mut Style) {
532 if index == self.tab.directory.index {
533 style.add_modifier |= Modifier::REVERSED;
534 }
535 }
536
537 fn color_searched(&self, file: &FileInfo, style: &mut Style) {
538 if self.tab.search.is_match(&file.filename) {
539 style.fg = MENU_STYLES
540 .get()
541 .expect("Menu style should be set")
542 .palette_4
543 .fg;
544 }
545 }
546
547 fn span_flagged_symbol<'b>(&self, file: &FileInfo, style: &mut Style) -> Span<'b> {
548 if self.status.menu.flagged.contains(&file.path) {
549 style.add_modifier |= Modifier::BOLD;
550 Span::styled(
551 "â–ˆ",
552 MENU_STYLES.get().expect("Menu colors should be set").second,
553 )
554 } else {
555 Span::raw("")
556 }
557 }
558}
559
560type Formater = fn(&FileInfo, (usize, usize)) -> String;
561
562struct FileFormater;
563
564impl FileFormater {
565 fn metadata(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
566 file.format_base(owner_sizes.1, owner_sizes.0)
567 }
568
569 fn metadata_no_group(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
570 file.format_no_group(owner_sizes.1)
571 }
572
573 fn metadata_no_permissions(file: &FileInfo, owner_sizes: (usize, usize)) -> String {
574 file.format_no_permissions(owner_sizes.1)
575 }
576
577 fn metadata_no_owner(file: &FileInfo, _owner_sizes: (usize, usize)) -> String {
578 file.format_no_owner()
579 }
580
581 fn simple(_file: &FileInfo, _owner_sizes: (usize, usize)) -> String {
582 " ".to_owned()
583 }
584}
585
586#[derive(Debug)]
587enum FormatKind {
588 Metadata,
589 MetadataNoGroup,
590 MetadataNoPermissions,
591 MetadataNoOwner,
592 Simple,
593}
594
595impl FormatKind {
596 #[rustfmt::skip]
597 fn from_flags(
598 with_metadata: bool,
599 width: u16,
600 ) -> Self {
601 let wide_enough_for_group = width > 70;
602 let wide_enough_for_metadata = width > 50;
603 let wide_enough_for_permissions = width > 40;
604 let wide_enough_for_owner = width > 30;
605
606 match (
607 with_metadata,
608 wide_enough_for_group,
609 wide_enough_for_metadata,
610 wide_enough_for_permissions,
611 wide_enough_for_owner,
612 ) {
613 (true, true, _, _, _) => Self::Metadata,
614 (true, false, true, _, _) => Self::MetadataNoGroup,
615 (true, _, _, true, _) => Self::MetadataNoPermissions,
616 (true, _, _, _, true) => Self::MetadataNoOwner,
617 _ => Self::Simple,
618 }
619 }
620}
621
622struct TreeDisplay<'a> {
623 status: &'a Status,
624 tab: &'a Tab,
625}
626
627impl<'a> Draw for TreeDisplay<'a> {
628 fn draw(&self, f: &mut Frame, rect: &Rect) {
629 self.tree(f, rect)
630 }
631}
632
633impl<'a> TreeDisplay<'a> {
634 fn new(files: &'a Files) -> Self {
635 Self {
636 status: files.status,
637 tab: files.tab,
638 }
639 }
640
641 fn tree(&self, f: &mut Frame, rect: &Rect) {
642 Self::tree_content(
643 self.status,
644 &self.tab.tree,
645 &self.tab.users,
646 &self.tab.window,
647 self.status.session.metadata(),
648 f,
649 rect,
650 )
651 }
652
653 fn tree_content(
654 status: &Status,
655 tree: &Tree,
656 users: &Users,
657 window: &ContentWindow,
658 with_metadata: bool,
659 f: &mut Frame,
660 rect: &Rect,
661 ) {
662 let p_rect = rect.offseted(1, 0);
663 let width = p_rect.width.saturating_sub(6);
664 let formater = DirectoryDisplay::pick_formater(with_metadata, width);
665 let with_icon = Self::use_icon(with_metadata);
666 let lines: Vec<_> = tree
667 .lines_enum_skip_take(window)
668 .filter_map(|(index, line_builder)| {
669 Self::tree_line(
670 status,
671 index == 0,
672 line_builder,
673 &formater,
674 users,
675 with_icon,
676 )
677 .ok()
678 })
679 .collect();
680 Paragraph::new(lines).render(p_rect, f.buffer_mut());
681 }
682
683 fn use_icon(with_metadata: bool) -> bool {
684 (!with_metadata && with_icon()) || with_icon_metadata()
685 }
686
687 fn tree_line<'b>(
688 status: &Status,
689 with_offset: bool,
690 line_builder: &'b TLine,
691 formater: &Formater,
692 users: &Users,
693 with_icon: bool,
694 ) -> Result<Line<'b>> {
695 let path = line_builder.path();
696 let fileinfo = FileInfo::new(&line_builder.path, users)?;
697 let mut style = fileinfo.style();
698 Self::reverse_flagged(line_builder, &mut style);
699 Self::color_searched(status, &fileinfo, &mut style);
700 Ok(Line::from(vec![
701 Self::span_flagged_symbol(status, path, &mut style),
702 Self::metadata(&fileinfo, formater, style),
703 Self::prefix(line_builder),
704 Self::whitespaces(status, path, with_offset),
705 Self::filename(line_builder, with_icon, style),
706 ]))
707 }
708
709 fn reverse_flagged(line_builder: &TLine, style: &mut Style) {
710 if line_builder.is_selected {
711 style.add_modifier |= Modifier::REVERSED;
712 }
713 }
714
715 fn color_searched(status: &Status, file: &FileInfo, style: &mut Style) {
716 if status.current_tab().search.is_match(&file.filename) {
717 style.fg = MENU_STYLES
718 .get()
719 .expect("Menu style should be set")
720 .palette_4
721 .fg;
722 }
723 }
724
725 fn span_flagged_symbol<'b>(
726 status: &Status,
727 path: &std::path::Path,
728 style: &mut Style,
729 ) -> Span<'b> {
730 if status.menu.flagged.contains(path) {
731 style.add_modifier |= Modifier::BOLD;
732 Span::styled(
733 "â–ˆ",
734 MENU_STYLES.get().expect("Menu colors should be set").second,
735 )
736 } else {
737 Span::raw(" ")
738 }
739 }
740
741 fn metadata<'b>(fileinfo: &FileInfo, formater: &Formater, style: Style) -> Span<'b> {
742 Span::styled(formater(fileinfo, (6, 6)), style)
743 }
744
745 fn prefix(line_builder: &TLine) -> Span<'_> {
746 Span::raw(line_builder.prefix())
747 }
748
749 fn whitespaces<'b>(status: &Status, path: &std::path::Path, with_offset: bool) -> Span<'b> {
750 Span::raw(" ".repeat(status.menu.flagged.contains(path) as usize + with_offset as usize))
751 }
752
753 fn filename<'b>(line_builder: &TLine, with_icon: bool, style: Style) -> Span<'b> {
754 Span::styled(line_builder.filename(with_icon), style)
755 }
756}
757
758struct PreviewDisplay<'a> {
759 status: &'a Status,
760 tab: &'a Tab,
761}
762
763impl<'a> PreviewDisplay<'a> {
771 fn draw(&self, f: &mut Frame, rect: &Rect, image_adapter: &mut ImageAdapter) {
772 self.preview(f, rect, image_adapter)
773 }
774}
775
776impl<'a> PreviewDisplay<'a> {
777 fn new(files: &'a Files) -> Self {
778 Self {
779 status: files.status,
780 tab: files.tab,
781 }
782 }
783
784 fn new_with_args(status: &'a Status, tab: &'a Tab) -> Self {
785 Self { status, tab }
786 }
787
788 fn preview(&self, f: &mut Frame, rect: &Rect, image_adapter: &mut ImageAdapter) {
789 let tab = self.tab;
790 let window = &tab.window;
791 let length = tab.preview.len();
792 match &tab.preview {
793 Preview::Syntaxed(syntaxed) => {
794 let number_col_width = Self::number_width(length);
795 self.syntaxed(f, syntaxed, length, rect, number_col_width, window)
796 }
797 Preview::Binary(bin) => self.binary(f, bin, length, rect, window),
798 Preview::Image(image) => self.image(image, rect, image_adapter),
799 Preview::Tree(tree_preview) => self.tree_preview(f, tree_preview, window, rect),
800 Preview::Text(ansi_text)
801 if matches!(ansi_text.kind, TextKind::CommandStdout | TextKind::Plugin) =>
802 {
803 self.ansi_text(f, ansi_text, length, rect, window)
804 }
805 Preview::Text(text) => self.normal_text(f, text, length, rect, window),
806
807 Preview::Empty => (),
808 };
809 }
810
811 fn line_number_span<'b>(
812 line_number_to_print: &usize,
813 number_col_width: usize,
814 style: Style,
815 ) -> Span<'b> {
816 Span::styled(
817 format!("{line_number_to_print:>number_col_width$} "),
818 style,
819 )
820 }
821
822 fn number_width(mut number: usize) -> usize {
824 let mut width = 0;
825 while number != 0 {
826 width += 1;
827 number /= 10;
828 }
829 width
830 }
831
832 fn normal_text(
834 &self,
835 f: &mut Frame,
836 text: &Text,
837 length: usize,
838 rect: &Rect,
839 window: &ContentWindow,
840 ) {
841 let p_rect = rect.offseted(2, 0);
842 let lines: Vec<_> = text
843 .take_skip(window.top, window.bottom, length)
844 .map(Line::raw)
845 .collect();
846 Paragraph::new(lines).render(p_rect, f.buffer_mut());
847 }
848
849 fn syntaxed(
850 &self,
851 f: &mut Frame,
852 syntaxed: &HLContent,
853 length: usize,
854 rect: &Rect,
855 number_col_width: usize,
856 window: &ContentWindow,
857 ) {
858 let p_rect = rect.offseted(3, 0);
859 let number_col_style = MENU_STYLES.get().expect("").first;
860 let lines: Vec<_> = syntaxed
861 .take_skip_enum(window.top, window.bottom, length)
862 .map(|(index, vec_line)| {
863 let mut line = vec![Self::line_number_span(
864 &index,
865 number_col_width,
866 number_col_style,
867 )];
868 line.append(
869 &mut vec_line
870 .iter()
871 .map(|token| Span::styled(&token.content, token.style))
872 .collect::<Vec<_>>(),
873 );
874 Line::from(line)
875 })
876 .collect();
877 Paragraph::new(lines).render(p_rect, f.buffer_mut());
878 }
879
880 fn binary(
881 &self,
882 f: &mut Frame,
883 bin: &BinaryContent,
884 length: usize,
885 rect: &Rect,
886 window: &ContentWindow,
887 ) {
888 let p_rect = rect.offseted(3, 0);
889 let line_number_width_hex = bin.number_width_hex();
890 let (style_number, style_ascii) = {
891 let ms = MENU_STYLES.get().expect("Menu colors should be set");
892 (ms.first, ms.second)
893 };
894 let lines: Vec<_> = (*bin)
895 .take_skip_enum(window.top, window.bottom, length)
896 .map(|(index, bin_line)| {
897 Line::from(vec![
898 Span::styled(
899 BinLine::format_line_nr_hex(index + 1 + window.top, line_number_width_hex),
900 style_number,
901 ),
902 Span::raw(bin_line.format_hex()),
903 Span::raw(" "),
904 Span::styled(bin_line.format_as_ascii(), style_ascii),
905 ])
906 })
907 .collect();
908 Paragraph::new(lines).render(p_rect, f.buffer_mut());
909 }
910
911 fn image(&self, image: &DisplayedImage, rect: &Rect, image_adapter: &mut ImageAdapter) {
914 if let Err(e) = image_adapter.draw(image, *rect) {
915 log_info!("Couldn't display {path}: {e:?}", path = image.identifier);
916 }
917 }
918
919 fn tree_preview(&self, f: &mut Frame, tree: &Tree, window: &ContentWindow, rect: &Rect) {
920 TreeDisplay::tree_content(self.status, tree, &self.tab.users, window, false, f, rect)
921 }
922
923 fn ansi_text(
924 &self,
925 f: &mut Frame,
926 ansi_text: &Text,
927 length: usize,
928 rect: &Rect,
929 window: &ContentWindow,
930 ) {
931 let p_rect = rect.offseted(3, 0);
932 let lines: Vec<_> = ansi_text
933 .take_skip(window.top, window.bottom, length)
934 .map(|line| {
935 Line::from(
936 AnsiString::parse(line)
937 .iter()
938 .map(|(chr, style)| Span::styled(chr.to_string(), style))
939 .collect::<Vec<_>>(),
940 )
941 })
942 .collect();
943 Paragraph::new(lines).render(p_rect, f.buffer_mut());
944 }
945}
946
947struct FilesHeader<'a> {
948 status: &'a Status,
949 tab: &'a Tab,
950 is_selected: bool,
951}
952
953impl<'a> Draw for FilesHeader<'a> {
954 fn draw(&self, f: &mut Frame, rect: &Rect) {
960 let width = rect.width;
961 let header: Box<dyn ClickableLine> = match self.tab.display_mode {
962 DisplayMode::Preview => Box::new(PreviewHeader::new(self.status, self.tab, width)),
963 _ => Box::new(Header::new(self.status, self.tab).expect("Couldn't build header")),
964 };
965 header.draw_left(f, *rect, self.is_selected);
966 header.draw_right(f, *rect, self.is_selected);
967 }
968}
969
970impl<'a> FilesHeader<'a> {
971 fn new(status: &'a Status, tab: &'a Tab, is_selected: bool) -> Self {
972 Self {
973 status,
974 tab,
975 is_selected,
976 }
977 }
978}
979
980#[derive(Default)]
981struct FilesSecondLine {
982 content: Option<String>,
983 style: Option<Style>,
984}
985
986impl Draw for FilesSecondLine {
987 fn draw(&self, f: &mut Frame, rect: &Rect) {
988 let p_rect = rect.offseted(1, 0);
989 if let (Some(content), Some(style)) = (&self.content, &self.style) {
990 Span::styled(content, *style).render(p_rect, f.buffer_mut());
991 };
992 }
993}
994
995impl FilesSecondLine {
996 fn new(status: &Status, tab: &Tab) -> Self {
997 if tab.display_mode.is_preview() || status.session.metadata() {
998 return Self::default();
999 };
1000 if let Ok(file) = tab.current_file() {
1001 Self::second_line_detailed(&file)
1002 } else {
1003 Self::default()
1004 }
1005 }
1006
1007 fn second_line_detailed(file: &FileInfo) -> Self {
1008 let owner_size = file.owner.len();
1009 let group_size = file.group.len();
1010 let mut style = file.style();
1011 style.add_modifier ^= Modifier::REVERSED;
1012
1013 Self {
1014 content: Some(file.format_metadata(owner_size, group_size)),
1015 style: Some(style),
1016 }
1017 }
1018}
1019
1020struct LogLine;
1021
1022impl Draw for LogLine {
1023 fn draw(&self, f: &mut Frame, rect: &Rect) {
1024 let p_rect = rect.offseted(4, 0);
1025 let log = &read_last_log_line();
1026 Span::styled(
1027 log,
1028 MENU_STYLES.get().expect("Menu colors should be set").second,
1029 )
1030 .render(p_rect, f.buffer_mut());
1031 }
1032}
1033
1034struct FilesFooter<'a> {
1035 status: &'a Status,
1036 tab: &'a Tab,
1037 is_selected: bool,
1038}
1039
1040impl<'a> Draw for FilesFooter<'a> {
1041 fn draw(&self, f: &mut Frame, rect: &Rect) {
1049 match self.tab.display_mode {
1050 DisplayMode::Preview => (),
1051 _ => {
1052 let Ok(footer) = Footer::new(self.status, self.tab) else {
1053 return;
1054 };
1055 footer.draw_left(f, *rect, self.is_selected);
1057 }
1058 }
1059 }
1060}
1061
1062impl<'a> FilesFooter<'a> {
1063 fn new(status: &'a Status, tab: &'a Tab, is_selected: bool) -> Self {
1064 Self {
1065 status,
1066 tab,
1067 is_selected,
1068 }
1069 }
1070}
1071
1072struct Menu<'a> {
1073 status: &'a Status,
1074 tab: &'a Tab,
1075}
1076
1077impl<'a> Draw for Menu<'a> {
1078 fn draw(&self, f: &mut Frame, rect: &Rect) {
1079 if !self.tab.need_menu_window() {
1080 return;
1081 }
1082 let mode = self.tab.menu_mode;
1083 self.cursor(f, rect);
1084 MenuFirstLine::new(self.status, rect).draw(f, rect);
1085 self.menu_line(f, rect);
1086 self.content_per_mode(f, rect, mode);
1087 self.binds_per_mode(f, rect, mode);
1088 }
1089}
1090
1091impl<'a> Menu<'a> {
1092 fn new(status: &'a Status, index: usize) -> Self {
1093 Self {
1094 status,
1095 tab: &status.tabs[index],
1096 }
1097 }
1098
1099 fn render_content<T>(content: &[T], f: &mut Frame, rect: &Rect, x: u16, y: u16)
1105 where
1106 T: AsRef<str>,
1107 {
1108 let p_rect = rect.offseted(x, y);
1109 let lines: Vec<_> = colored_iter!(content)
1110 .map(|(text, style)| Line::from(vec![Span::styled(text.as_ref(), style)]))
1111 .take(p_rect.height as usize + 2)
1112 .collect();
1113 Paragraph::new(lines).render(p_rect, f.buffer_mut());
1114 }
1115
1116 fn cursor(&self, f: &mut Frame, rect: &Rect) {
1123 if self.tab.menu_mode.show_cursor() {
1124 let offset = self.tab.menu_mode.cursor_offset();
1125 let avail = rect.width.saturating_sub(offset + 1) as usize;
1126 let cursor_index = self.status.menu.input.display_index(avail) as u16;
1127 let x = rect.x + offset + cursor_index;
1128 f.set_cursor_position(Position::new(x, rect.y));
1129 }
1130 }
1131
1132 fn menu_line(&self, f: &mut Frame, rect: &Rect) {
1133 let menu_style = MENU_STYLES.get().expect("Menu colors should be set");
1134 let menu = menu_style.second;
1135 match self.tab.menu_mode {
1136 MenuMode::InputSimple(InputSimple::Chmod) => {
1137 let first = menu_style.first;
1138 self.menu_line_chmod(f, rect, first, menu);
1139 }
1140 MenuMode::InputSimple(InputSimple::Remote) => {
1141 let palette_3 = menu_style.palette_3;
1142 self.menu_line_remote(f, rect, palette_3);
1143 }
1144 edit => {
1145 let rect = rect.offseted(2, 1);
1146 Span::styled(edit.second_line(), menu).render(rect, f.buffer_mut());
1147 }
1148 };
1149 }
1150
1151 fn menu_line_chmod(&self, f: &mut Frame, rect: &Rect, first: Style, menu: Style) {
1152 let input = self.status.menu.input.string();
1153 let (text, is_valid) = parse_input_permission(&input);
1154 let style = if is_valid { first } else { menu };
1155 let p_rect = rect.offseted(11, 1);
1156 Line::styled(text.as_ref(), style).render(p_rect, f.buffer_mut());
1157 }
1158
1159 fn menu_line_remote(&self, f: &mut Frame, rect: &Rect, first: Style) {
1160 let input = self.status.menu.input.string();
1161 let current_path = path_to_string(&self.tab.current_directory_path());
1162
1163 if let Some(remote) = Remote::from_input(input, ¤t_path) {
1164 let command = format!("{command:?}", command = remote.command());
1165 let p_rect = rect.offseted(4, 8);
1166 Line::styled(command, first).render(p_rect, f.buffer_mut());
1167 };
1168 }
1169
1170 fn content_per_mode(&self, f: &mut Frame, rect: &Rect, mode: MenuMode) {
1171 match mode {
1172 MenuMode::Navigate(mode) => self.navigate(mode, f, rect),
1173 MenuMode::NeedConfirmation(mode) => self.confirm(mode, f, rect),
1174 MenuMode::InputCompleted(_) => self.completion(f, rect),
1175 MenuMode::InputSimple(mode) => Self::input_simple(mode.lines(), f, rect),
1176 _ => (),
1177 }
1178 }
1179
1180 fn binds_per_mode(&self, f: &mut Frame, rect: &Rect, mode: MenuMode) {
1181 if mode == MenuMode::Navigate(Navigate::Trash) {
1182 return;
1183 }
1184 let p_rect = rect.offseted(2, rect.height.saturating_sub(2));
1185 Span::styled(
1186 mode.binds_per_mode(),
1187 MENU_STYLES.get().expect("Menu colors should be set").second,
1188 )
1189 .render(p_rect, f.buffer_mut());
1190 }
1191
1192 fn input_simple(lines: &[&str], f: &mut Frame, rect: &Rect) {
1193 let mut p_rect = rect.offseted(4, ContentWindow::WINDOW_MARGIN_TOP_U16);
1194 p_rect.height = p_rect.height.saturating_sub(2);
1195 Self::render_content(lines, f, &p_rect, 0, 0);
1196 }
1197
1198 fn navigate(&self, navigate: Navigate, f: &mut Frame, rect: &Rect) {
1199 if navigate.simple_draw_menu() {
1200 return self.status.menu.draw_navigate(f, rect, navigate);
1201 }
1202 match navigate {
1203 Navigate::Cloud => self.cloud(f, rect),
1204 Navigate::Context => self.context(f, rect),
1205 Navigate::TempMarks(_) => self.temp_marks(f, rect),
1206 Navigate::Flagged => self.flagged(f, rect),
1207 Navigate::History => self.history(f, rect),
1208 Navigate::Picker => self.picker(f, rect),
1209 Navigate::Trash => self.trash(f, rect),
1210 _ => unreachable!("menu.simple_draw_menu should cover this mode"),
1211 }
1212 }
1213
1214 fn history(&self, f: &mut Frame, rect: &Rect) {
1215 let selectable = &self.tab.history;
1216 let mut window = ContentWindow::new(selectable.len(), rect.height as usize);
1217 window.scroll_to(selectable.index);
1218 selectable.draw_menu(f, rect, &window)
1219 }
1220
1221 fn trash(&self, f: &mut Frame, rect: &Rect) {
1222 let trash = &self.status.menu.trash;
1223 if trash.content().is_empty() {
1224 self.trash_is_empty(f, rect)
1225 } else {
1226 self.trash_content(f, rect, trash)
1227 };
1228 }
1229
1230 fn trash_content(&self, f: &mut Frame, rect: &Rect, trash: &Trash) {
1231 trash.draw_menu(f, rect, &self.status.menu.window);
1232
1233 let p_rect = rect.offseted(2, rect.height.saturating_sub(2));
1234 Span::styled(
1235 &trash.help,
1236 MENU_STYLES.get().expect("Menu colors should be set").second,
1237 )
1238 .render(p_rect, f.buffer_mut());
1239 }
1240
1241 fn trash_is_empty(&self, f: &mut Frame, rect: &Rect) {
1242 Self::content_line(
1243 f,
1244 rect,
1245 0,
1246 "Trash is empty",
1247 MENU_STYLES.get().expect("Menu colors should be set").second,
1248 );
1249 }
1250
1251 fn cloud(&self, f: &mut Frame, rect: &Rect) {
1252 let cloud = &self.status.menu.cloud;
1253 let mut desc = cloud.desc();
1254 if let Some((index, metadata)) = &cloud.metadata_repr {
1255 if index == &cloud.index {
1256 desc = format!("{desc} - {metadata}");
1257 }
1258 }
1259 let p_rect = rect.offseted(2, 2);
1260 Span::styled(
1261 desc,
1262 MENU_STYLES
1263 .get()
1264 .expect("Menu colors should be set")
1265 .palette_4,
1266 )
1267 .render(p_rect, f.buffer_mut());
1268 cloud.draw_menu(f, rect, &self.status.menu.window)
1269 }
1270
1271 fn picker(&self, f: &mut Frame, rect: &Rect) {
1272 let selectable = &self.status.menu.picker;
1273 selectable.draw_menu(f, rect, &self.status.menu.window);
1274 if let Some(desc) = &selectable.desc {
1275 let p_rect = rect.offseted(10, 0);
1276 Span::styled(
1277 desc,
1278 MENU_STYLES.get().expect("Menu colors should be set").first,
1279 )
1280 .render(p_rect, f.buffer_mut());
1281 }
1282 }
1283
1284 fn temp_marks(&self, f: &mut Frame, rect: &Rect) {
1285 let selectable = &self.status.menu.temp_marks;
1286 selectable.draw_menu(f, rect, &self.status.menu.window);
1287 }
1288
1289 fn context(&self, f: &mut Frame, rect: &Rect) {
1290 self.context_selectable(f, rect);
1291 self.context_more_infos(f, rect)
1292 }
1293
1294 fn context_selectable(&self, f: &mut Frame, rect: &Rect) {
1295 self.status
1296 .menu
1297 .context
1298 .draw_menu(f, rect, &self.status.menu.window);
1299 }
1300
1301 fn context_more_infos(&self, f: &mut Frame, rect: &Rect) {
1302 let Ok(file_info) = &self.tab.current_file() else {
1303 return;
1304 };
1305 let space_used = self.status.menu.context.content.len() as u16;
1306 let lines = MoreInfos::new(file_info, &self.status.internal_settings.opener).to_lines();
1307 let more_infos: Vec<&String> = lines.iter().filter(|line| !line.is_empty()).collect();
1308 Self::render_content(&more_infos, f, rect, 4, 3 + space_used);
1309 }
1310
1311 fn flagged(&self, f: &mut Frame, rect: &Rect) {
1312 self.flagged_files(f, rect);
1313 self.flagged_selected(f, rect);
1314 }
1315
1316 fn flagged_files(&self, f: &mut Frame, rect: &Rect) {
1317 self.status
1318 .menu
1319 .flagged
1320 .draw_menu(f, rect, &self.status.menu.window);
1321 }
1322
1323 fn flagged_selected(&self, f: &mut Frame, rect: &Rect) {
1324 if let Some(selected) = self.status.menu.flagged.selected() {
1325 let Ok(fileinfo) = FileInfo::new(selected, &self.tab.users) else {
1326 return;
1327 };
1328 let p_rect = rect.offseted(2, 2);
1329 Span::styled(fileinfo.format_metadata(6, 6), fileinfo.style())
1330 .render(p_rect, f.buffer_mut());
1331 };
1332 }
1333
1334 fn completion(&self, f: &mut Frame, rect: &Rect) {
1337 self.status
1338 .menu
1339 .completion
1340 .draw_menu(f, rect, &self.status.menu.window)
1341 }
1342
1343 fn confirm(&self, confirmed_mode: NeedConfirmation, f: &mut Frame, rect: &Rect) {
1345 let dest = path_to_string(
1346 &self
1347 .tab
1348 .directory_of_selected()
1349 .unwrap_or_else(|_| std::path::Path::new("")),
1350 );
1351
1352 Self::content_line(
1353 f,
1354 rect,
1355 0,
1356 &confirmed_mode.confirmation_string(&dest),
1357 MENU_STYLES.get().expect("Menu colors should be set").second,
1358 );
1359 match confirmed_mode {
1360 NeedConfirmation::EmptyTrash => self.confirm_empty_trash(f, rect),
1361 NeedConfirmation::BulkAction => self.confirm_bulk(f, rect),
1362 NeedConfirmation::DeleteCloud => self.confirm_delete_cloud(f, rect),
1363 _ => self.confirm_default(f, rect),
1364 };
1365 }
1366
1367 fn confirm_default(&self, f: &mut Frame, rect: &Rect) {
1368 self.status
1369 .menu
1370 .flagged
1371 .draw_menu(f, rect, &self.status.menu.window);
1372 }
1373
1374 fn confirm_bulk(&self, f: &mut Frame, rect: &Rect) {
1375 let content = self.status.menu.bulk.format_confirmation();
1376
1377 let mut p_rect = rect.offseted(4, 3);
1378 p_rect.height = p_rect.height.saturating_sub(2);
1379 let window = &self.status.menu.window;
1381 use std::cmp::min;
1382 let lines: Vec<_> = colored_skip_take!(content, window)
1383 .map(|(index, item, style)| {
1384 Line::styled(item, self.status.menu.bulk.style(index, &style))
1385 })
1386 .collect();
1387 Paragraph::new(lines).render(p_rect, f.buffer_mut());
1388 }
1389
1390 fn confirm_delete_cloud(&self, f: &mut Frame, rect: &Rect) {
1391 let line = if let Some(selected) = &self.status.menu.cloud.selected() {
1392 &format!(
1393 "{desc}{sel}",
1394 desc = self.status.menu.cloud.desc(),
1395 sel = selected.path()
1396 )
1397 } else {
1398 "No selected file"
1399 };
1400 Self::content_line(
1401 f,
1402 rect,
1403 3,
1404 line,
1405 MENU_STYLES
1406 .get()
1407 .context("MENU_STYLES should be set")
1408 .expect("Couldn't read MENU_STYLES")
1409 .palette_4,
1410 );
1411 }
1412
1413 fn confirm_empty_trash(&self, f: &mut Frame, rect: &Rect) {
1414 if self.status.menu.trash.is_empty() {
1415 self.trash_is_empty(f, rect)
1416 } else {
1417 self.confirm_non_empty_trash(f, rect)
1418 }
1419 }
1420
1421 fn confirm_non_empty_trash(&self, f: &mut Frame, rect: &Rect) {
1422 self.status
1423 .menu
1424 .trash
1425 .draw_menu(f, rect, &self.status.menu.window);
1426 }
1427
1428 fn content_line(f: &mut Frame, rect: &Rect, row: u16, text: &str, style: Style) {
1429 let p_rect = rect.offseted(4, row + ContentWindow::WINDOW_MARGIN_TOP_U16);
1430 Span::styled(text, style).render(p_rect, f.buffer_mut());
1431 }
1432}
1433
1434pub struct MenuFirstLine {
1437 content: Vec<String>,
1438}
1439
1440impl Draw for MenuFirstLine {
1441 fn draw(&self, f: &mut Frame, rect: &Rect) {
1442 let spans: Vec<_> = std::iter::zip(
1443 self.content.iter(),
1444 MENU_STYLES
1445 .get()
1446 .expect("Menu colors should be set")
1447 .palette()
1448 .iter()
1449 .cycle(),
1450 )
1451 .map(|(text, style)| Span::styled(text, *style))
1452 .collect();
1453 let p_rect = rect.offseted(Self::LEFT_MARGIN, 0);
1454 Line::from(spans).render(p_rect, f.buffer_mut());
1455 }
1456}
1457
1458impl MenuFirstLine {
1459 pub const LEFT_MARGIN: u16 = 2;
1461
1462 fn new(status: &Status, rect: &Rect) -> Self {
1463 Self {
1464 content: status.current_tab().menu_mode.line_display(status, rect),
1465 }
1466 }
1467}
1468
1469struct Rects;
1471
1472impl Rects {
1473 const FILES_WITH_LOGLINE: &[Constraint] = &[
1474 Constraint::Length(1),
1475 Constraint::Length(1),
1476 Constraint::Fill(1),
1477 Constraint::Length(1),
1478 Constraint::Length(1),
1479 ];
1480
1481 const FILES_WITHOUT_LOGLINE: &[Constraint] = &[
1482 Constraint::Length(1),
1483 Constraint::Length(1),
1484 Constraint::Fill(1),
1485 Constraint::Length(1),
1486 ];
1487
1488 fn full_rect(width: u16, height: u16) -> Rect {
1490 Rect::new(0, 0, width, height)
1491 }
1492
1493 fn inside_border_rect(width: u16, height: u16) -> Rect {
1495 Rect::new(1, 1, width.saturating_sub(2), height.saturating_sub(2))
1496 }
1497
1498 fn left_right_inside_rects(rect: Rect) -> Rc<[Rect]> {
1500 Layout::new(
1501 Direction::Horizontal,
1502 [Constraint::Min(rect.width / 2), Constraint::Fill(1)],
1503 )
1504 .split(rect)
1505 }
1506
1507 fn dual_bordered_rect(
1509 parent_wins: Rc<[Rect]>,
1510 have_menu_left: bool,
1511 have_menu_right: bool,
1512 ) -> Vec<Rect> {
1513 let mut bordered_wins =
1514 Self::vertical_split_border(parent_wins[0], have_menu_left).to_vec();
1515 bordered_wins
1516 .append(&mut Self::vertical_split_border(parent_wins[1], have_menu_right).to_vec());
1517 bordered_wins
1518 }
1519
1520 fn dual_inside_rect(rect: Rect, have_menu_left: bool, have_menu_right: bool) -> Vec<Rect> {
1522 let left_right = Self::left_right_rects(rect);
1523 let mut areas = Self::vertical_split_inner(left_right[0], have_menu_left).to_vec();
1524 areas.append(&mut Self::vertical_split_inner(left_right[2], have_menu_right).to_vec());
1525 areas
1526 }
1527
1528 fn left_right_rects(rect: Rect) -> Rc<[Rect]> {
1531 Layout::new(
1532 Direction::Horizontal,
1533 [
1534 Constraint::Min(rect.width / 2 - 1),
1535 Constraint::Max(2),
1536 Constraint::Min(rect.width / 2 - 2),
1537 ],
1538 )
1539 .split(rect)
1540 }
1541
1542 fn vertical_split_inner(parent_win: Rect, have_menu: bool) -> Rc<[Rect]> {
1545 if have_menu {
1546 Layout::new(
1547 Direction::Vertical,
1548 [
1549 Constraint::Min(parent_win.height / 2 - 1),
1550 Constraint::Max(2),
1551 Constraint::Fill(1),
1552 ],
1553 )
1554 .split(parent_win)
1555 } else {
1556 Rc::new([parent_win, Rect::default(), Rect::default()])
1557 }
1558 }
1559
1560 fn vertical_split_border(parent_win: Rect, have_menu: bool) -> Rc<[Rect]> {
1562 let percent = if have_menu { 50 } else { 100 };
1563 Layout::new(
1564 Direction::Vertical,
1565 [Constraint::Percentage(percent), Constraint::Fill(1)],
1566 )
1567 .split(parent_win)
1568 }
1569
1570 fn files(rect: &Rect, use_log_line: bool) -> Rc<[Rect]> {
1577 Layout::new(
1578 Direction::Vertical,
1579 if use_log_line {
1580 Self::FILES_WITH_LOGLINE
1581 } else {
1582 Self::FILES_WITHOUT_LOGLINE
1583 },
1584 )
1585 .split(*rect)
1586 }
1587
1588 fn fuzzy(area: &Rect) -> Rc<[Rect]> {
1589 Layout::default()
1590 .direction(Direction::Vertical)
1591 .constraints([Constraint::Length(1), Constraint::Min(0)])
1592 .split(*area)
1593 }
1594}
1595
1596pub struct Display {
1599 term: Terminal<CrosstermBackend<Stdout>>,
1602 image_adapter: ImageAdapter,
1604}
1605
1606impl Display {
1607 pub fn new(term: Terminal<CrosstermBackend<Stdout>>) -> Self {
1609 log_info!("starting display...");
1610 let image_adapter = ImageAdapter::detect();
1611 Self {
1612 term,
1613 image_adapter,
1614 }
1615 }
1616
1617 pub fn display_all(&mut self, status: &MutexGuard<Status>) {
1635 io::stdout().flush().expect("Couldn't flush the stdout");
1636 if status.should_tabs_images_be_cleared() {
1637 self.clear_images();
1638 }
1639 if status.should_be_cleared() {
1640 self.term.clear().expect("Couldn't clear the terminal");
1641 }
1642 let Ok(Size { width, height }) = self.term.size() else {
1643 return;
1644 };
1645 let full_rect = Rects::full_rect(width, height);
1646 let inside_border_rect = Rects::inside_border_rect(width, height);
1647 let borders = Self::borders(status);
1648 if Self::use_dual_pane(status, width) {
1649 self.draw_dual(full_rect, inside_border_rect, borders, status);
1650 } else {
1651 self.draw_single(full_rect, inside_border_rect, borders, status);
1652 };
1653 }
1654
1655 fn borders(status: &Status) -> [Style; 4] {
1657 let menu_styles = MENU_STYLES.get().expect("MENU_STYLES should be set");
1658 let mut borders = [menu_styles.inert_border; 4];
1659 let selected_border = menu_styles.selected_border;
1660 borders[status.focus.index()] = selected_border;
1661 borders
1662 }
1663
1664 fn use_dual_pane(status: &Status, width: u16) -> bool {
1666 status.session.dual() && width > MIN_WIDTH_FOR_DUAL_PANE
1667 }
1668
1669 fn draw_dual(
1670 &mut self,
1671 full_rect: Rect,
1672 inside_border_rect: Rect,
1673 borders: [Style; 4],
1674 status: &Status,
1675 ) {
1676 let (file_left, file_right) = FilesBuilder::dual(status);
1677 let menu_left = Menu::new(status, 0);
1678 let menu_right = Menu::new(status, 1);
1679 let parent_wins = Rects::left_right_inside_rects(full_rect);
1680 let have_menu_left = status.tabs[0].need_menu_window();
1681 let have_menu_right = status.tabs[1].need_menu_window();
1682 let bordered_wins = Rects::dual_bordered_rect(parent_wins, have_menu_left, have_menu_right);
1683 let inside_wins =
1684 Rects::dual_inside_rect(inside_border_rect, have_menu_left, have_menu_right);
1685 self.render_dual(
1686 borders,
1687 bordered_wins,
1688 inside_wins,
1689 (file_left, file_right),
1690 (menu_left, menu_right),
1691 );
1692 }
1693
1694 fn render_dual(
1695 &mut self,
1696 borders: [Style; 4],
1697 bordered_wins: Vec<Rect>,
1698 inside_wins: Vec<Rect>,
1699 files: (Files, Files),
1700 menus: (Menu, Menu),
1701 ) {
1702 let _ = self.term.draw(|f| {
1703 Self::draw_dual_borders(borders, f, &bordered_wins);
1707 files.0.draw(f, &inside_wins[0], &mut self.image_adapter);
1708 menus.0.draw(f, &inside_wins[2]);
1709 files.1.draw(f, &inside_wins[3], &mut self.image_adapter);
1710 menus.1.draw(f, &inside_wins[5]);
1711 });
1712 }
1713
1714 fn draw_single(
1715 &mut self,
1716 rect: Rect,
1717 inside_border_rect: Rect,
1718 borders: [Style; 4],
1719 status: &Status,
1720 ) {
1721 let file_left = FilesBuilder::single(status);
1722 let menu_left = Menu::new(status, 0);
1723 let need_menu = status.tabs[0].need_menu_window();
1724 let bordered_wins = Rects::vertical_split_border(rect, need_menu);
1725 let inside_wins = Rects::vertical_split_inner(inside_border_rect, need_menu);
1726 self.render_single(borders, bordered_wins, inside_wins, file_left, menu_left)
1727 }
1728
1729 fn render_single(
1730 &mut self,
1731 borders: [Style; 4],
1732 bordered_wins: Rc<[Rect]>,
1733 inside_wins: Rc<[Rect]>,
1734 file_left: Files,
1735 menu_left: Menu,
1736 ) {
1737 let _ = self.term.draw(|f| {
1738 Self::draw_single_borders(borders, f, &bordered_wins);
1739 file_left.draw(f, &inside_wins[0], &mut self.image_adapter);
1740 menu_left.draw(f, &inside_wins[2]);
1741 });
1742 }
1743
1744 fn draw_n_borders(n: usize, borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1745 for i in 0..n {
1746 let bordered_block = Block::default()
1747 .borders(Borders::ALL)
1748 .border_style(borders[i]);
1749 f.render_widget(bordered_block, wins[i]);
1750 }
1751 }
1752
1753 fn draw_dual_borders(borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1754 Self::draw_n_borders(4, borders, f, wins)
1755 }
1756
1757 fn draw_single_borders(borders: [Style; 4], f: &mut Frame, wins: &[Rect]) {
1758 Self::draw_n_borders(2, borders, f, wins)
1759 }
1760
1761 pub fn clear_images(&mut self) {
1763 log_info!("display.clear_images()");
1764 self.image_adapter
1765 .clear_all()
1766 .expect("Couldn't clear all the images");
1767 self.term.clear().expect("Couldn't clear the terminal");
1768 }
1769
1770 pub fn restore_terminal(&mut self) -> Result<()> {
1775 disable_raw_mode()?;
1776 execute!(self.term.backend_mut(), LeaveAlternateScreen)?;
1777 self.term.show_cursor()?;
1778 Ok(())
1779 }
1780}