Skip to main content

diskr/ui/
widgets.rs

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}