pulsedeck 0.2.1

A focused terminal internet radio player with fast search, saved stations, themes, visualizers, and resilient playback
use crate::app::App;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Clear, Paragraph};

use super::{critical, theme};

const MIN_RECENT_WIDTH: u16 = 56;
const MIN_RECENT_HEIGHT: u16 = 12;
const MAX_VISIBLE_TRACKS: usize = 10;

pub fn render(frame: &mut Frame, area: Rect, app: &App) {
    let popup_area = super::centered_rect(62, 52, area);

    if recent_area_is_compact(popup_area) {
        frame.render_widget(Clear, popup_area);
        super::render_boundary_warning(
            frame,
            popup_area,
            "Recent Tracks Too Compact",
            format!(
                "Expand terminal or close recent tracks (overlay: {}x{})",
                popup_area.width, popup_area.height
            ),
        );
        return;
    }

    frame.render_widget(Clear, popup_area);

    let block = Block::default()
        .title(Span::styled(" ✦ Recent Tracks ✦ ", theme::title()))
        .borders(Borders::ALL)
        .border_style(
            Style::default()
                .fg(theme::accent_secondary())
                .add_modifier(Modifier::BOLD),
        )
        .border_type(ratatui::widgets::BorderType::Rounded)
        .style(theme::clear());

    let inner_area = block.inner(popup_area);
    let (content_area, alert_area) = critical::split_overlay_alert_area(inner_area, &app.playback);
    frame.render_widget(block, popup_area);

    let paragraph =
        Paragraph::new(recent_track_lines(app)).wrap(ratatui::widgets::Wrap { trim: true });
    frame.render_widget(paragraph, content_area);

    if let Some(alert_area) = alert_area {
        critical::render_engine_fault_banner(frame, alert_area, &app.playback);
    }
}

fn recent_area_is_compact(area: Rect) -> bool {
    area.width < MIN_RECENT_WIDTH || area.height < MIN_RECENT_HEIGHT
}

fn recent_track_lines(app: &App) -> Vec<Line<'static>> {
    if app.song_history.is_empty() {
        return vec![
            Line::from(Span::styled("No track titles heard yet", theme::title())),
            Line::from(""),
            Line::from(Span::styled(
                "PulseDeck will list stream-provided track titles here while you listen.",
                theme::dim(),
            )),
            Line::from(""),
            close_hint(),
        ];
    }

    let mut lines = Vec::new();
    for (idx, track) in app
        .song_history
        .iter()
        .rev()
        .take(MAX_VISIBLE_TRACKS)
        .enumerate()
    {
        lines.push(Line::from(vec![
            Span::styled(format!("{:>2}. ", idx + 1), theme::dim()),
            Span::styled(track.clone(), theme::text()),
        ]));
    }

    lines.push(Line::from(""));
    lines.push(Line::from(Span::styled(
        "Session-only list; nothing is recorded or archived.",
        theme::dim(),
    )));
    lines.push(close_hint());
    lines
}

fn close_hint() -> Line<'static> {
    Line::from(vec![
        Span::styled(" g ", theme::cyan()),
        Span::styled("or", theme::dim()),
        Span::styled(" Esc/q ", theme::cyan()),
        Span::styled("closes this panel", theme::dim()),
    ])
}

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

    #[test]
    fn recent_overlay_rejects_tiny_area() {
        assert!(recent_area_is_compact(Rect::new(0, 0, 55, 12)));
        assert!(recent_area_is_compact(Rect::new(0, 0, 56, 11)));
    }

    #[test]
    fn recent_overlay_accepts_minimum_area() {
        assert!(!recent_area_is_compact(Rect::new(0, 0, 56, 12)));
    }
}