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