Files
ui-agregator/src/ui/views/calendar.rs
T
Alexander 620bd374de refactor: replace vim keybindings with mouse navigation, remove mock data
- Remove all sample/mock data (artists, albums, tracks, queue, history, calendar)
- Delete input module (vim.rs, leader.rs) and related UI (which_key, cmdline)
- Add mouse support: click tabs, click list items, scroll wheel navigation
- Show real disk space via nix::statvfs instead of hardcoded value
- Simplify topbar/statusbar by removing mode display and key hints
- Hide album/track sections when no artist is selected
2026-05-08 22:16:38 +02:00

207 lines
6.1 KiB
Rust

//! 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<CalendarEntry>,
}
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<CalendarCell> = 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<CalendarEntry> = calendar
.iter()
.filter(|e| {
if let Some(day_str) = e.date.split('-').nth(2) {
if let Ok(day) = day_str.parse::<u8>() {
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<Span> = 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<Line> = 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);
}