shuvarie 0.1.1

Blazingly fast AI coding TUI for chivalrous people
use ratatui::prelude::Stylize;
use ratatui::text::{Line, Span};
use ratatui::widgets::ListItem;

use super::theme;

pub fn scroll_offset_for(
    selected: usize,
    old_offset: usize,
    viewport_height: usize,
    len: usize,
) -> usize {
    if len == 0 || viewport_height == 0 {
        return 0;
    }
    let selected = selected.min(len - 1);
    let max_offset = len.saturating_sub(viewport_height);
    if selected < old_offset {
        selected
    } else if selected >= old_offset + viewport_height {
        selected.saturating_sub(viewport_height - 1).min(max_offset)
    } else {
        old_offset.min(max_offset)
    }
}

pub fn render_list_item(content: String, is_selected: bool) -> ListItem<'static> {
    let prefix: &'static str = if is_selected { "" } else { "  " };
    let line = Line::from(vec![
        Span::raw(prefix).fg(theme::accent()),
        Span::raw(content).fg(theme::text()),
    ]);
    let style = if is_selected {
        ratatui::style::Style::new()
            .bg(theme::accent_bg())
            .fg(theme::text())
    } else {
        ratatui::style::Style::new()
    };
    ListItem::new(line).style(style)
}

pub fn render_list_item_line(line: Line<'static>, is_selected: bool) -> ListItem<'static> {
    let prefix: &'static str = if is_selected { "" } else { "  " };
    let mut spans = vec![Span::raw(prefix).fg(theme::accent())];
    spans.extend(line.spans);
    let style = if is_selected {
        ratatui::style::Style::new()
            .bg(theme::accent_bg())
            .fg(theme::text())
    } else {
        ratatui::style::Style::new()
    };
    ListItem::new(Line::from(spans)).style(style)
}

pub fn render_list_item_dim(content: String, is_selected: bool) -> ListItem<'static> {
    let prefix: &'static str = if is_selected { "" } else { "  " };
    let line = Line::from(vec![
        Span::raw(prefix).fg(theme::text_muted()),
        Span::raw(content).fg(theme::text_dim()),
    ]);
    let style = if is_selected {
        ratatui::style::Style::new()
            .bg(theme::accent_bg())
            .fg(theme::text_dim())
    } else {
        ratatui::style::Style::new()
    };
    ListItem::new(line).style(style)
}

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

    #[test]
    fn offset_no_scroll_when_visible() {
        assert_eq!(scroll_offset_for(2, 0, 5, 10), 0);
    }

    #[test]
    fn offset_scrolls_down_when_selection_below_view() {
        assert_eq!(scroll_offset_for(7, 0, 5, 10), 3);
    }

    #[test]
    fn offset_scrolls_up_when_selection_above_view() {
        assert_eq!(scroll_offset_for(1, 5, 5, 10), 1);
    }

    #[test]
    fn offset_unchanged_when_selection_in_view() {
        assert_eq!(scroll_offset_for(6, 5, 5, 10), 5);
    }

    #[test]
    fn offset_empty_list_is_zero() {
        assert_eq!(scroll_offset_for(0, 0, 5, 0), 0);
    }

    #[test]
    fn offset_zero_viewport_is_zero() {
        assert_eq!(scroll_offset_for(5, 0, 0, 10), 0);
    }

    #[test]
    fn offset_at_last_item() {
        assert_eq!(scroll_offset_for(9, 5, 5, 10), 5);
    }

    #[test]
    fn offset_clamps_to_max() {
        assert_eq!(scroll_offset_for(9, 0, 5, 10), 5);
    }

    #[test]
    fn offset_selection_above_old_offset_returns_selected() {
        assert_eq!(scroll_offset_for(0, 3, 5, 10), 0);
    }
}