//! The calendar tab's popup for switching built-in holiday calendars on and off,
//! and setting how the motd reminds of them
use crate::data::ReminderTarget;
use crate::do_tui::reminder_spans;
use crate::entities::calendar;
use crate::holidays::{self, about, CalendarDef, Group, Holiday, Kind, GROUPS};
use crate::tui_add::popup_area;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Constraint, Layout};
use ratatui::prelude::{Line, Span, Style, Stylize};
use ratatui::style::palette::tailwind::{AMBER, CYAN, GREEN, SLATE, VIOLET, WHITE};
use ratatui::style::Color;
use ratatui::widgets::{Block, Clear, List, ListItem, ListState, Paragraph, Wrap};
use ratatui::Frame;
use std::io;
use std::process::{Command, Stdio};
enum Row {
/// A group heading, which holds the group's motd defaults
Group(&'static Group),
Cal(&'static CalendarDef),
}
impl Row {
fn target(&self) -> ReminderTarget {
match self {
Row::Group(g) => ReminderTarget::Group(g),
Row::Cal(c) => ReminderTarget::Calendar(c),
}
}
}
/// A country's states only show while the country is on, to keep the list short
fn rows(enabled: &[String]) -> Vec<Row> {
let on = |c: &CalendarDef| enabled.iter().any(|k| k == c.key);
let mut rows = vec![];
for g in &GROUPS {
rows.push(Row::Group(g));
for c in holidays::in_group(g) {
rows.push(Row::Cal(c));
if on(c) {
rows.extend(holidays::regions(c.key).map(Row::Cal));
}
}
}
rows
}
/// The colour a calendar's holidays get in the month grid and the upcoming list
pub fn holiday_color(h: &Holiday) -> Color {
match h.calendar.kind {
Kind::Religious => VIOLET.c400,
Kind::Other => AMBER.c300,
Kind::Subscribed => GREEN.c400,
_ => CYAN.c400,
}
}
pub enum MenuAction {
Continue,
Close,
Toggle(&'static CalendarDef),
/// One of the reminder keys, on a calendar or a group
Reminder(ReminderTarget, char),
}
#[derive(Default)]
pub struct CalendarMenu {
state: ListState,
}
impl CalendarMenu {
pub fn new() -> Self {
let mut menu = CalendarMenu::default();
// Row 0 is the countries heading; start on the first country
menu.state.select(Some(1));
menu
}
fn selected<'a>(&self, rows: &'a [Row]) -> Option<&'a Row> {
rows.get(self.state.selected()?)
}
pub fn handle_key(&mut self, key: &KeyEvent, enabled: &[String]) -> MenuAction {
let rows = rows(enabled);
match key.code {
KeyCode::Esc | KeyCode::Char('c') | KeyCode::Char('C') | KeyCode::Char('q') => {
MenuAction::Close
}
KeyCode::Up | KeyCode::Char('k') => {
self.state.scroll_up_by(1);
MenuAction::Continue
}
KeyCode::Down | KeyCode::Char('j') => {
self.state.scroll_down_by(1);
MenuAction::Continue
}
KeyCode::Enter | KeyCode::Char(' ') => match self.selected(&rows) {
Some(Row::Cal(c)) => MenuAction::Toggle(c),
_ => MenuAction::Continue,
},
KeyCode::Char(k @ ('m' | '+' | '-' | '[' | ']' | 'x')) => match self.selected(&rows) {
Some(r) => MenuAction::Reminder(r.target(), k),
None => MenuAction::Continue,
},
_ => MenuAction::Continue,
}
}
pub fn render(&mut self, f: &mut Frame, enabled: &[String], settings: &[calendar::Model]) {
let rows = rows(enabled);
// Keep the cursor in range when a country's states disappear
if self.state.selected().is_none_or(|i| i >= rows.len()) {
self.state.select(Some(rows.len().saturating_sub(1)));
}
let note = match self.selected(&rows) {
Some(Row::Cal(c)) => c.note,
Some(Row::Group(_)) => Some("motd defaults for the calendars below"),
None => None,
};
let area = popup_area(f.area(), 84, rows.len() as u16 + 6);
f.render_widget(Clear, area);
let block = Block::bordered()
.title(" built-in calendars ")
.style(Style::default().fg(AMBER.c500));
let inner = block.inner(area);
f.render_widget(block, area);
let [list_area, help_area] =
Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).areas(inner);
// "⏰ 7d · daily", dimmed where it comes from a default
let spans = |t: ReminderTarget| {
let r = t.reminder(settings);
reminder_spans(r.delta, r.remind, r.default_delta, r.default_remind)
};
let items = rows
.iter()
.map(|r| match r {
Row::Group(g) => {
let mut line =
Line::from(Span::styled(format!(" {}", g.name), SLATE.c400).bold());
for s in spans(r.target()) {
line.push_span(s);
}
ListItem::new(line)
}
Row::Cal(c) => {
let on = enabled.iter().any(|k| k == c.key);
let indent = if c.kind == Kind::Region { " " } else { "" };
let (mark, color) = match (on, c.kind) {
(false, _) => ("☐", SLATE.c500),
(true, Kind::Religious) => ("✓", VIOLET.c400),
(true, Kind::Other) => ("✓", AMBER.c300),
(true, Kind::Subscribed) => ("✓", GREEN.c400),
(true, _) => ("✓", CYAN.c400),
};
let mut line = Line::from(vec![
Span::styled(format!(" {}{} ", indent, mark), color),
Span::styled(c.name, if on { WHITE } else { SLATE.c400 }),
Span::styled(format!(" {}", c.key), SLATE.c600),
]);
if on {
for s in spans(r.target()) {
line.push_span(s);
}
}
ListItem::new(line)
}
})
.collect::<Vec<_>>();
let list = List::new(items).highlight_symbol(">>");
f.render_stateful_widget(list, list_area, &mut self.state);
let mut help = vec![];
if let Some(n) = note {
help.push(Line::styled(format!(" {}", n), SLATE.c400).italic());
}
help.push(Line::styled(
" space: on/off · motd: +/- days ahead · m: mode · [/]: random% · x: use default · esc: close",
SLATE.c500,
));
f.render_widget(Paragraph::new(help).wrap(Wrap { trim: false }), help_area);
}
}
/// The `i` popup: what a holiday is, and a link to read more
pub fn render_holiday_info(f: &mut Frame, h: &Holiday) {
let info = about(h);
let area = popup_area(f.area(), 72, 18);
f.render_widget(Clear, area);
let color = holiday_color(h);
let mut lines = vec![
Line::from(vec![
Span::styled(h.date.format("%A %-d %B %Y").to_string(), WHITE),
Span::styled(format!(" · {}", h.calendar.name), color),
]),
Line::raw(""),
Line::styled(info.blurb, WHITE),
Line::raw(""),
];
// A subscription's note is where it comes from, not a caveat
if let Some(note) = h
.calendar
.note
.filter(|_| h.calendar.kind != Kind::Subscribed)
{
lines.push(Line::styled(note, SLATE.c400).italic());
lines.push(Line::raw(""));
}
match info.url.is_empty() {
true => lines.push(Line::styled("any key: close", SLATE.c500)),
false => {
lines.push(Line::styled(info.url, CYAN.c300).underlined());
lines.push(Line::styled(
"o: open in browser · any other key: close",
SLATE.c500,
));
}
}
let popup = Paragraph::new(lines).wrap(Wrap { trim: true }).block(
Block::bordered()
.title(format!(" {} ", h.name))
.style(Style::default().fg(color)),
);
f.render_widget(popup, area);
}
/// Open a link in the desktop's browser, without waiting for it
pub fn open_url(url: &str) -> io::Result<()> {
let opener = match std::env::consts::OS {
"macos" => "open",
_ => "xdg-open",
};
Command::new(opener)
.arg(url)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map(|_| ())
}
⭐
Log in to mark this repository.
8.5 KiB · text
History
2c2327e