liblitho 0.2.0

cli tool to flash/clone the images to storage devices
Documentation
use ratatui::layout::{Constraint, Direction, Layout, Rect};

pub const MIN_COLS: u16 = 60;
pub const MIN_ROWS: u16 = 24;

pub const PANEL_WIDTH_FULL: u16 = 80;
pub const HEADER_HEIGHT: u16 = 5;
pub const FOOTER_HEIGHT: u16 = 2;

pub struct UiLayout {
    pub header: Rect,
    pub main_card: Rect,
    pub footer: Rect,
    pub too_small: bool,
    pub compact: bool,
    pub show_shortcut_hints: bool,
}

pub fn terminal_too_small(area: Rect) -> bool {
    area.width < MIN_COLS || area.height < MIN_ROWS
}

pub fn compute_layout(area: Rect) -> UiLayout {
    if terminal_too_small(area) {
        return UiLayout {
            header: Rect::default(),
            main_card: Rect::default(),
            footer: Rect::default(),
            too_small: true,
            compact: true,
            show_shortcut_hints: false,
        };
    }

    let compact = area.width < PANEL_WIDTH_FULL || area.height < 40;
    let show_shortcut_hints = !compact && area.height >= 30;
    let panel_width = area
        .width
        .saturating_sub(2)
        .clamp(MIN_COLS - 2, PANEL_WIDTH_FULL);

    let main_card_height = if compact {
        area.height
            .saturating_sub(HEADER_HEIGHT + FOOTER_HEIGHT + 4)
            .max(12)
    } else {
        area.height
            .saturating_sub(HEADER_HEIGHT + FOOTER_HEIGHT + 4)
            .clamp(20, 52)
    };

    let panel_height = HEADER_HEIGHT + main_card_height + FOOTER_HEIGHT + 1;
    let top_pad = area.height.saturating_sub(panel_height) / 2;

    let outer = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(top_pad),
            Constraint::Length(panel_height.min(area.height)),
            Constraint::Min(0),
        ])
        .split(area);

    let h_pad = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Min(0),
            Constraint::Length(panel_width.min(outer[1].width)),
            Constraint::Min(0),
        ])
        .split(outer[1]);

    let panel = h_pad[1];

    let sections = Layout::default()
        .direction(Direction::Vertical)
        .spacing(1)
        .constraints([
            Constraint::Length(HEADER_HEIGHT.min(panel.height)),
            Constraint::Min(
                main_card_height.min(panel.height.saturating_sub(HEADER_HEIGHT + FOOTER_HEIGHT)),
            ),
            Constraint::Length(FOOTER_HEIGHT.min(panel.height)),
        ])
        .split(panel);

    UiLayout {
        header: sections[0],
        main_card: sections[1],
        footer: sections[2],
        too_small: false,
        compact,
        show_shortcut_hints,
    }
}

/// Vertical chunks inside the main card. Flash mode includes VERIFY rows; clone omits them.
pub fn main_card_constraints(compact: bool, include_verify: bool) -> Vec<Constraint> {
    let (mode_cards, device_info, file_select, verify_option, status, progress_bar, controls) =
        if compact {
            (5, 2, 4, 3, 4, 2, 3)
        } else {
            (7, 3, 5, 3, 5, 3, 4)
        };

    let mut constraints = vec![
        Constraint::Length(1),
        Constraint::Length(mode_cards),
        Constraint::Length(1),
        Constraint::Length(3),
        Constraint::Length(device_info),
        Constraint::Length(1),
        Constraint::Length(file_select),
    ];

    if include_verify {
        constraints.push(Constraint::Length(1));
        constraints.push(Constraint::Length(verify_option));
    }

    constraints.extend([
        Constraint::Length(status),
        Constraint::Length(1),
        Constraint::Length(progress_bar),
        Constraint::Length(controls),
    ]);

    constraints
}

pub fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
    let width = width.min(area.width.saturating_sub(2)).max(1);
    let height = height.min(area.height.saturating_sub(2)).max(1);
    let x = area.x + area.width.saturating_sub(width) / 2;
    let y = area.y + area.height.saturating_sub(height) / 2;
    Rect::new(x, y, width, height)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn rect(w: u16, h: u16) -> Rect {
        Rect::new(0, 0, w, h)
    }

    #[test]
    fn terminal_too_small_below_minimum() {
        assert!(terminal_too_small(rect(59, 24)));
        assert!(terminal_too_small(rect(60, 23)));
        assert!(!terminal_too_small(rect(60, 24)));
    }

    #[test]
    fn compute_layout_too_small_gate() {
        let layout = compute_layout(rect(50, 20));
        assert!(layout.too_small);
        assert_eq!(layout.header, Rect::default());
    }

    #[test]
    fn compute_layout_fits_within_parent() {
        let area = rect(100, 50);
        let layout = compute_layout(area);
        assert!(!layout.too_small);
        assert!(layout.header.y >= area.y);
        assert!(layout.header.y + layout.header.height <= area.y + area.height);
        assert!(layout.main_card.y + layout.main_card.height <= area.y + area.height);
        assert!(layout.footer.y + layout.footer.height <= area.y + area.height);
    }

    #[test]
    fn show_shortcut_hints_when_terminal_tall_enough() {
        let layout = compute_layout(rect(100, 40));
        assert!(layout.show_shortcut_hints);
        let small = compute_layout(rect(100, 28));
        assert!(!small.show_shortcut_hints);
    }

    #[test]
    fn centered_rect_never_exceeds_area() {
        let area = rect(40, 20);
        let popup = centered_rect(100, 100, area);
        assert!(popup.width <= area.width);
        assert!(popup.height <= area.height);
        assert!(popup.x >= area.x);
        assert!(popup.y >= area.y);
    }
}