//! Calendar view - upcoming releases. use ratatui::{ Frame, layout::{Constraint, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, widgets::Paragraph, }; use crate::data::CalendarEntry; use crate::theme; use crate::ui::pane::Pane; struct CalendarCell { day: u8, dim: bool, is_today: bool, events: Vec, } pub fn render_calendar(frame: &mut Frame, area: Rect, calendar: &[CalendarEntry]) { let meta = "upcoming releases · May 2026"; let footer = Line::from(vec![ Span::styled("[h/l]", Style::default().fg(theme::GRAY)), Span::styled(" month · ", Style::default().fg(theme::FG2)), Span::styled("[Enter]", Style::default().fg(theme::GRAY)), Span::styled(" details", Style::default().fg(theme::FG2)), Span::raw(" "), Span::styled( format!("{} upcoming", calendar.len()), Style::default().fg(theme::GRAY), ), ]); let pane = Pane::new("Calendar") .meta(meta) .focused(true) .footer(footer); let block = pane.build_block(); let inner = block.inner(area); frame.render_widget(block, area); // Build calendar grid for May 2026 let today_day = 8u8; // May 8, 2026 let year = 2026u16; let month = 5u8; // May 2026 starts on Friday (day_of_week = 5) let start_dow = 5u8; let days_in_month = 31u8; let days_in_prev = 30u8; // April has 30 days let mut cells: Vec = Vec::new(); // Previous month days for i in 0..start_dow { cells.push(CalendarCell { day: days_in_prev - start_dow + i + 1, dim: true, is_today: false, events: Vec::new(), }); } // Current month days for d in 1..=days_in_month { let events: Vec = calendar .iter() .filter(|e| { if let Some(day_str) = e.date.split('-').nth(2) { if let Ok(day) = day_str.parse::() { let month_match = e.date.contains(&format!("{:04}-{:02}", year, month)); return month_match && day == d; } } false }) .cloned() .collect(); cells.push(CalendarCell { day: d, dim: false, is_today: d == today_day, events, }); } // Next month days to fill 6 weeks (42 cells) let mut next_day = 1u8; while cells.len() < 42 { cells.push(CalendarCell { day: next_day, dim: true, is_today: false, events: Vec::new(), }); next_day += 1; } // Layout: header + day-of-week + 6 rows of days let chunks = Layout::vertical([ Constraint::Length(1), // Month header Constraint::Length(1), // Day of week labels Constraint::Fill(1), // Calendar grid ]) .split(inner); // Month header: "◀ May 2026 ▶" let header = Line::from(vec![ Span::styled("◀ ", Style::default().fg(theme::GRAY)), Span::styled( "May 2026", Style::default() .fg(theme::YELLOW) .add_modifier(Modifier::BOLD), ), Span::styled(" ▶", Style::default().fg(theme::GRAY)), ]); frame.render_widget( Paragraph::new(header).alignment(ratatui::layout::Alignment::Center), chunks[0], ); // Day of week labels let dow_labels = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; let cell_width = chunks[2].width / 7; let dow_spans: Vec = dow_labels .iter() .map(|&d| { let pad = cell_width.saturating_sub(3) as usize; Span::styled( format!("{}{}", d, " ".repeat(pad)), Style::default().fg(theme::GRAY), ) }) .collect(); frame.render_widget(Paragraph::new(Line::from(dow_spans)), chunks[1]); // Calendar grid - 6 rows x 7 cols let row_height = chunks[2].height / 6; let grid_rows = Layout::vertical(vec![Constraint::Length(row_height); 6]).split(chunks[2]); for (row_idx, row_area) in grid_rows.iter().enumerate() { let col_areas = Layout::horizontal(vec![Constraint::Ratio(1, 7); 7]).split(*row_area); for (col_idx, col_area) in col_areas.iter().enumerate() { let cell_idx = row_idx * 7 + col_idx; if cell_idx < cells.len() { render_calendar_cell(frame, *col_area, &cells[cell_idx]); } } } } fn render_calendar_cell(frame: &mut Frame, area: Rect, cell: &CalendarCell) { if area.height == 0 || area.width == 0 { return; } let day_style = if cell.dim { Style::default().fg(theme::BG4) } else if cell.is_today { Style::default().fg(theme::FG1) } else { Style::default().fg(theme::FG2) }; let mut lines: Vec = Vec::new(); // First line: day number with optional today marker let day_line = if cell.is_today { Line::from(vec![ Span::styled(format!("{}", cell.day), day_style), Span::styled(" ●", Style::default().fg(theme::ORANGE)), ]) } else { Line::from(Span::styled(format!("{}", cell.day), day_style)) }; lines.push(day_line); // Event lines let max_events = (area.height.saturating_sub(1)) as usize; for event in cell.events.iter().take(max_events) { let event_style = match event.status.as_str() { "announced" => Style::default().fg(theme::YELLOW), "monitored" => Style::default().fg(theme::GREEN), _ => Style::default().fg(theme::FG2), }; let mut display = event.artist.clone(); let max_len = area.width.saturating_sub(1) as usize; if display.len() > max_len { display.truncate(max_len.saturating_sub(1)); display.push('…'); } lines.push(Line::from(Span::styled(display, event_style))); } let para = Paragraph::new(lines); frame.render_widget(para, area); }