1use crate::ui::app::App;
2use humansize::{format_size, DECIMAL};
3use ratatui::layout::{Constraint, Direction, Layout, Rect};
4use ratatui::style::{Color, Modifier, Style};
5use ratatui::text::{Line, Span};
6use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
7use ratatui::Frame;
8
9pub fn draw(f: &mut Frame, app: &mut App) {
10 let chunks = Layout::default()
11 .direction(Direction::Vertical)
12 .constraints([Constraint::Length(3), Constraint::Min(3), Constraint::Length(3)])
13 .split(f.area());
14
15 draw_header(f, chunks[0], app);
16 draw_list(f, chunks[1], app);
17 draw_footer(f, chunks[2], app);
18}
19
20fn draw_header(f: &mut Frame, area: Rect, app: &App) {
21 let node = app.current();
22 let text = Line::from(vec![
23 Span::styled(app.breadcrumb(), Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
24 Span::raw(" "),
25 Span::styled(format!("({})", format_size(node.size, DECIMAL)), Style::default().fg(Color::Yellow)),
26 ]);
27 let p = Paragraph::new(text).block(Block::default().borders(Borders::ALL).title(" diskr "));
28 f.render_widget(p, area);
29}
30
31fn draw_list(f: &mut Frame, area: Rect, app: &mut App) {
32 let visible = area.height.saturating_sub(2) as usize;
33 app.set_visible_rows(visible);
34 app.list_area = area;
35
36 let node = app.current();
37 let total = node.children.len();
38 let max_size = node.children.iter().map(|c| c.size).max().unwrap_or(1).max(1);
39
40 let start = app.scroll.min(total.saturating_sub(1).max(0));
41 let end = (start + app.visible_rows).min(total);
42
43 let items: Vec<ListItem> = node.children[start..end]
44 .iter()
45 .enumerate()
46 .map(|(offset, c)| {
47 let i = start + offset;
48 let ratio = (c.size as f64 / max_size as f64 * 20.0) as usize;
49 let bar: String = "█".repeat(ratio.min(20));
50 let is_marked = app.marked.contains(&c.path);
51 let marker = if is_marked { "✓" } else { " " };
52 let name = c
53 .path
54 .file_name()
55 .map(|n| n.to_string_lossy().to_string())
56 .unwrap_or_else(|| c.path.display().to_string());
57 let (kind_suffix, name_color) = if c.is_dir {
58 ("/", Color::LightBlue)
59 } else {
60 ("", Color::White)
61 };
62
63 let marker_color = if is_marked { Color::Green } else { Color::DarkGray };
64 let line = Line::from(vec![
65 Span::styled(format!(" {marker} "), Style::default().fg(marker_color).add_modifier(Modifier::BOLD)),
66 Span::styled(format!("{:>10} ", format_size(c.size, DECIMAL)), Style::default().fg(Color::Gray)),
67 Span::styled(format!("{bar:<20}"), Style::default().fg(Color::Cyan)),
68 Span::raw(" "),
69 Span::styled(format!("{name}{kind_suffix}"), Style::default().fg(name_color)),
70 ]);
71
72 let style = if i == app.selected {
73 Style::default().add_modifier(Modifier::REVERSED)
74 } else {
75 Style::default()
76 };
77 ListItem::new(line).style(style)
78 })
79 .collect();
80
81 let title = if total == 0 {
82 " contents (empty) ".to_string()
83 } else {
84 format!(" contents {}/{} ", app.selected + 1, total)
85 };
86
87 let list = List::new(items).block(Block::default().borders(Borders::ALL).title(title));
88 f.render_widget(list, area);
89}
90
91fn draw_footer(f: &mut Frame, area: Rect, app: &App) {
92 let mut spans = vec![Span::styled(
93 "j/k move l/enter open h/⌫ back g/G top/bottom pgup/pgdn page space mark q quit click/scroll mouse",
94 Style::default().fg(Color::DarkGray),
95 )];
96 if !app.marked.is_empty() {
97 spans.push(Span::raw(" "));
98 spans.push(Span::styled(
99 format!("{} marked ({})", app.marked.len(), format_size(app.marked_total(), DECIMAL)),
100 Style::default().fg(Color::Green).add_modifier(Modifier::BOLD),
101 ));
102 }
103 let p = Paragraph::new(Line::from(spans)).block(Block::default().borders(Borders::ALL));
104 f.render_widget(p, area);
105}