diskr-cli 1.0.0

Save your disk space, without fear of deleting the wrong thing.
Documentation
use crate::ui::app::App;
use humansize::{format_size, DECIMAL};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
use ratatui::Frame;

pub fn draw(f: &mut Frame, app: &mut App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(3), Constraint::Min(3), Constraint::Length(3)])
        .split(f.area());

    draw_header(f, chunks[0], app);
    draw_list(f, chunks[1], app);
    draw_footer(f, chunks[2], app);
}

fn draw_header(f: &mut Frame, area: Rect, app: &App) {
    let node = app.current();
    let text = Line::from(vec![
        Span::styled(app.breadcrumb(), Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
        Span::raw("  "),
        Span::styled(format!("({})", format_size(node.size, DECIMAL)), Style::default().fg(Color::Yellow)),
    ]);
    let p = Paragraph::new(text).block(Block::default().borders(Borders::ALL).title(" diskr "));
    f.render_widget(p, area);
}

fn draw_list(f: &mut Frame, area: Rect, app: &mut App) {
    let visible = area.height.saturating_sub(2) as usize;
    app.set_visible_rows(visible);
    app.list_area = area;

    let node = app.current();
    let total = node.children.len();
    let max_size = node.children.iter().map(|c| c.size).max().unwrap_or(1).max(1);

    let start = app.scroll.min(total.saturating_sub(1).max(0));
    let end = (start + app.visible_rows).min(total);

    let items: Vec<ListItem> = node.children[start..end]
        .iter()
        .enumerate()
        .map(|(offset, c)| {
            let i = start + offset;
            let ratio = (c.size as f64 / max_size as f64 * 20.0) as usize;
            let bar: String = "█".repeat(ratio.min(20));
            let is_marked = app.marked.contains(&c.path);
            let marker = if is_marked { "✓" } else { " " };
            let name = c
                .path
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_else(|| c.path.display().to_string());
            let (kind_suffix, name_color) = if c.is_dir {
                ("/", Color::LightBlue)
            } else {
                ("", Color::White)
            };

            let marker_color = if is_marked { Color::Green } else { Color::DarkGray };
            let line = Line::from(vec![
                Span::styled(format!(" {marker} "), Style::default().fg(marker_color).add_modifier(Modifier::BOLD)),
                Span::styled(format!("{:>10}  ", format_size(c.size, DECIMAL)), Style::default().fg(Color::Gray)),
                Span::styled(format!("{bar:<20}"), Style::default().fg(Color::Cyan)),
                Span::raw("  "),
                Span::styled(format!("{name}{kind_suffix}"), Style::default().fg(name_color)),
            ]);

            let style = if i == app.selected {
                Style::default().add_modifier(Modifier::REVERSED)
            } else {
                Style::default()
            };
            ListItem::new(line).style(style)
        })
        .collect();

    let title = if total == 0 {
        " contents (empty) ".to_string()
    } else {
        format!(" contents  {}/{} ", app.selected + 1, total)
    };

    let list = List::new(items).block(Block::default().borders(Borders::ALL).title(title));
    f.render_widget(list, area);
}

fn draw_footer(f: &mut Frame, area: Rect, app: &App) {
    let mut spans = vec![Span::styled(
        "j/k move   l/enter open   h/⌫ back   g/G top/bottom   pgup/pgdn page   space mark   q quit   click/scroll mouse",
        Style::default().fg(Color::DarkGray),
    )];
    if !app.marked.is_empty() {
        spans.push(Span::raw("   "));
        spans.push(Span::styled(
            format!("{} marked ({})", app.marked.len(), format_size(app.marked_total(), DECIMAL)),
            Style::default().fg(Color::Green).add_modifier(Modifier::BOLD),
        ));
    }
    let p = Paragraph::new(Line::from(spans)).block(Block::default().borders(Borders::ALL));
    f.render_widget(p, area);
}