audium 2.0.0

A terminal music app
pub mod layout;
pub mod lyrics_overlay;
pub mod overlay;
pub mod player_bar;
pub mod queue;
pub mod sidebar;
pub mod tracklist;

use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout},
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Paragraph},
};

use crate::app::AppState;

const TOP_BAR_HEIGHT: u16 = 1;

/// Top-bar segments: name flush left, tagline centred, hint flush right.
const BAR_NAME: &str = " audium";
const BAR_TAGLINE: &str = "terminal music app";
const BAR_HINT: &str = "[?] help ";

pub fn render(frame: &mut Frame<'_>, state: &AppState) {
    let area = frame.area();

    frame.render_widget(
        Block::default().style(state.theme.apply_bg(Style::default())),
        area,
    );

    // with nothing playing the bar has nothing to report, so it yields its
    // rows to the lists
    let player_h = if state.now_playing.is_some() {
        player_bar::PLAYER_BAR_H
    } else {
        0
    };

    let vertical = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(TOP_BAR_HEIGHT),
            Constraint::Min(0),
            Constraint::Length(player_h),
        ])
        .split(area);

    render_top_bar(frame, vertical[0], state);

    let body_area = vertical[1];
    let horizontal = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Length(28), Constraint::Min(0)])
        .split(body_area);

    let content_split = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
        .split(horizontal[1]);

    sidebar::render_sidebar(frame, state, horizontal[0]);
    tracklist::render_tracklist(frame, state, content_split[0]);
    queue::render_queue(frame, state, content_split[1]);
    if player_h > 0 {
        player_bar::render_player_bar(frame, state, vertical[2]);
    }
    overlay::render_overlay(frame, state);
}

/// Name flush left, tagline centred against the *whole* bar, one hint flush
/// right. `?` is the only shortcut advertised: it lists every binding there
/// is, so repeating a handful beside it would only add noise.
fn render_top_bar(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &AppState) {
    let t = &state.theme;
    let width = area.width as usize;
    // centred on the bar, not on the space left after the name, which would
    // drift whenever either end changed length
    let tagline_start = width.saturating_sub(BAR_TAGLINE.len()) / 2;
    let lead = tagline_start.saturating_sub(BAR_NAME.len());
    let trail = width.saturating_sub(BAR_NAME.len() + lead + BAR_TAGLINE.len() + BAR_HINT.len());

    // too narrow for all three: keep the ends, drop the tagline
    let spans = if lead == 0 || trail == 0 {
        let gap = width.saturating_sub(BAR_NAME.len() + BAR_HINT.len());
        vec![
            Span::styled(
                BAR_NAME,
                Style::default().fg(t.accent).add_modifier(Modifier::BOLD),
            ),
            Span::raw(" ".repeat(gap)),
            Span::styled(BAR_HINT, Style::default().fg(t.subtle)),
        ]
    } else {
        vec![
            Span::styled(
                BAR_NAME,
                Style::default().fg(t.accent).add_modifier(Modifier::BOLD),
            ),
            Span::raw(" ".repeat(lead)),
            Span::styled(BAR_TAGLINE, Style::default().fg(t.text_dim)),
            Span::raw(" ".repeat(trail)),
            Span::styled(BAR_HINT, Style::default().fg(t.subtle)),
        ]
    };

    frame.render_widget(
        Paragraph::new(Line::from(spans)).style(t.apply_bg(Style::default())),
        area,
    );
}