upgate-presentation 0.1.1

Keep globally installed developer tools up to date
Documentation
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::{Line, Span};
use ratatui::widgets::{List, ListItem, ListState};

use super::scrollbar::render_vertical_scrollbar;
use crate::tui::components::render_separator;
use crate::tui::theme::TuiTheme;

pub struct CommandLogLayout {
    pub main: Rect,
    pub separator: Rect,
    pub log: Rect,
}

pub fn command_log_layout(enabled: bool, area: Rect) -> Option<CommandLogLayout> {
    let height = command_log_height(enabled, area)?;
    let [main, separator, log] = Layout::vertical([
        Constraint::Fill(1),
        Constraint::Length(1),
        Constraint::Length(height),
    ])
    .areas(area);

    Some(CommandLogLayout {
        main,
        separator,
        log,
    })
}

pub fn render_command_log(
    frame: &mut Frame<'_>,
    separator_area: Rect,
    log_area: Rect,
    commands: &[String],
    scroll_from_bottom: usize,
    theme: &TuiTheme,
) {
    render_separator(frame, separator_area, theme);

    let visible_height = usize::from(log_area.height);
    let scroll_from_bottom =
        clamp_command_log_scroll(scroll_from_bottom, commands.len(), visible_height);
    let max_offset = commands.len().saturating_sub(visible_height);
    let offset_from_top = max_offset.saturating_sub(scroll_from_bottom);
    let items = commands.iter().map(|command| {
        ListItem::new(Line::from(Span::styled(
            format!("$ {command}"),
            theme.muted,
        )))
    });
    let mut state = ListState::default().with_offset(offset_from_top);
    frame.render_stateful_widget(List::new(items), log_area, &mut state);
    render_vertical_scrollbar(
        frame,
        log_area,
        commands.len(),
        offset_from_top,
        visible_height,
        theme,
    );
}

pub fn clamp_command_log_scroll(
    scroll_from_bottom: usize,
    command_count: usize,
    visible_height: usize,
) -> usize {
    scroll_from_bottom.min(command_count.saturating_sub(visible_height))
}

const fn command_log_height(enabled: bool, area: Rect) -> Option<u16> {
    if !enabled {
        return None;
    }

    match area.height {
        0..=5 => None,
        6 => Some(3),
        _ => Some(4),
    }
}