pub mod controls;
pub mod critical;
pub mod deck;
pub mod header;
pub mod help;
pub mod recent_tracks;
pub mod search;
pub mod settings;
pub mod station_details;
pub mod stations;
pub mod theme;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::app::{App, InputMode, LayoutMode};
const MIN_REQUIRED_WIDTH: u16 = 80;
const MIN_REQUIRED_HEIGHT: u16 = 24;
pub fn draw(frame: &mut Frame, app: &App) {
let size = frame.area();
let bg = Block::default().style(theme::clear());
frame.render_widget(bg, size);
if is_compact_terminal(size) {
render_compact_terminal_warning(frame, size);
return;
}
let is_searching = app.input_mode == InputMode::Search;
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Min(5), Constraint::Length(1), Constraint::Length(2), ])
.split(size);
header::render(frame, chunks[0], app);
render_separator(frame, chunks[1]);
match app.layout_mode {
LayoutMode::Split => {
let content_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
.split(chunks[2]);
let left_area = content_chunks[0];
let right_area = content_chunks[1];
deck::render(frame, right_area, app);
if is_searching {
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(left_area);
search::render(frame, left_chunks[0], app);
stations::render(frame, left_chunks[1], app);
} else {
stations::render(frame, left_area, app);
}
}
LayoutMode::LeftOnly => {
if is_searching {
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(chunks[2]);
search::render(frame, left_chunks[0], app);
stations::render(frame, left_chunks[1], app);
} else {
stations::render(frame, chunks[2], app);
}
}
LayoutMode::RightOnly => {
deck::render(frame, chunks[2], app);
}
}
render_separator(frame, chunks[3]);
controls::render(frame, chunks[4], app);
if app.show_station_details {
station_details::render(frame, size, app);
}
if app.show_recent_tracks {
recent_tracks::render(frame, size, app);
}
if app.show_help {
help::render(frame, size, app);
}
if app.show_settings {
settings::render(frame, size, app);
}
}
pub fn is_compact_terminal(size: Rect) -> bool {
size.width < MIN_REQUIRED_WIDTH || size.height < MIN_REQUIRED_HEIGHT
}
fn render_compact_terminal_warning(frame: &mut Frame, area: Rect) {
render_boundary_warning(
frame,
area,
"Console Screen Too Compact",
format!(
"Expand terminal to at least {}x{} (current: {}x{})",
MIN_REQUIRED_WIDTH, MIN_REQUIRED_HEIGHT, area.width, area.height
),
);
}
pub fn render_boundary_warning(frame: &mut Frame, area: Rect, title: &str, detail: String) {
let diagnostic_area = centered_rect(90, 40, area);
let warning = Paragraph::new(vec![
Line::from(Span::styled(title.to_string(), theme::title())),
Line::from(""),
Line::from(Span::styled(detail, theme::dim())),
])
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(theme::border())
.border_type(ratatui::widgets::BorderType::Rounded)
.style(theme::clear()),
);
frame.render_widget(warning, diagnostic_area);
}
fn render_separator(frame: &mut Frame, area: Rect) {
let width = area.width as usize;
let line_str = "═".repeat(width);
let sep = Paragraph::new(Line::from(Span::styled(
line_str,
Style::default().fg(theme::accent()).bg(theme::bg()),
)));
frame.render_widget(sep, area);
}
pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compact_terminal_rejects_width_below_minimum() {
assert!(is_compact_terminal(Rect::new(0, 0, 79, 24)));
}
#[test]
fn compact_terminal_rejects_height_below_minimum() {
assert!(is_compact_terminal(Rect::new(0, 0, 80, 23)));
}
#[test]
fn compact_terminal_accepts_exact_minimum() {
assert!(!is_compact_terminal(Rect::new(0, 0, 80, 24)));
}
#[test]
fn compact_terminal_accepts_larger_terminal() {
assert!(!is_compact_terminal(Rect::new(0, 0, 120, 40)));
}
}