//! The calendar tab's `i` popup: what a date is, and its notes and todos
use crate::entities::note;
use crate::holidays::{about, Holiday, Kind as CalKind};
use crate::notes::{self, Kind, Occurrence};
use crate::tui_add::popup_area;
use crate::tui_calendars::holiday_color;
use crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::prelude::{Line, Span, Style, Stylize};
use ratatui::style::palette::tailwind::{CYAN, ORANGE, PINK, SLATE, WHITE};
use ratatui::widgets::{Block, Clear, Paragraph, Wrap};
use ratatui::Frame;
use tui_input::backend::crossterm::EventHandler;
use tui_input::Input;
pub enum PopupAction {
Continue,
Close,
Open(String),
Add(Kind, String),
Toggle(i32),
Delete(i32),
Status(String),
}
pub struct NotePopup {
pub occ: Occurrence,
/// Set for holidays and iCal events, which have a blurb and maybe a link
pub holiday: Option<Holiday>,
cursor: usize,
/// Typing a new note or todo
input: Option<(Kind, Input)>,
/// The note Delete was pressed on once
pending_delete: Option<i32>,
}
impl NotePopup {
pub fn new(occ: Occurrence, holiday: Option<Holiday>) -> Self {
NotePopup {
occ,
holiday,
cursor: 0,
input: None,
pending_delete: None,
}
}
pub fn handle_key(&mut self, key: &KeyEvent, all: &[note::Model]) -> PopupAction {
let mine = self.occ.notes(all);
let pending = self.pending_delete.take();
if let Some((kind, input)) = self.input.as_mut() {
return match key.code {
KeyCode::Esc => {
self.input = None;
PopupAction::Continue
}
KeyCode::Enter => {
let action = PopupAction::Add(*kind, input.value().to_string());
self.input = None;
// The new one goes to the end of its kind; follow it there
self.cursor = mine.len();
action
}
_ => {
input.handle_event(&Event::Key(*key));
PopupAction::Continue
}
};
}
let selected = mine.get(self.cursor.min(mine.len().saturating_sub(1)));
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('i') => PopupAction::Close,
KeyCode::Char('n') => {
self.input = Some((Kind::Note, Input::default()));
PopupAction::Continue
}
KeyCode::Char('t') => {
self.input = Some((Kind::Todo, Input::default()));
PopupAction::Continue
}
KeyCode::Up | KeyCode::Char('k') => {
self.cursor = self.cursor.saturating_sub(1);
PopupAction::Continue
}
KeyCode::Down | KeyCode::Char('j') => {
self.cursor = (self.cursor + 1).min(mine.len().saturating_sub(1));
PopupAction::Continue
}
KeyCode::Char(' ') | KeyCode::Enter => match selected {
Some(n) if n.kind == "todo" => PopupAction::Toggle(n.id),
_ => PopupAction::Continue,
},
KeyCode::Delete | KeyCode::Backspace => match selected {
Some(n) if pending == Some(n.id) => PopupAction::Delete(n.id),
Some(n) => {
self.pending_delete = Some(n.id);
PopupAction::Status(format!("⚠ press Delete again to remove \"{}\"", n.text))
}
None => PopupAction::Continue,
},
KeyCode::Char('o') => {
match self
.holiday
.as_ref()
.map(|h| about(h).url)
.filter(|u| !u.is_empty())
{
Some(url) => PopupAction::Open(url),
None => PopupAction::Status("ℹ this has no link".to_string()),
}
}
_ => PopupAction::Continue,
}
}
pub fn render(&self, f: &mut Frame, all: &[note::Model]) {
let mine = self.occ.notes(all);
let color = match &self.holiday {
Some(h) => holiday_color(h),
None => PINK.c400,
};
let mut lines = vec![Line::from(vec![
Span::styled(self.occ.date.format("%A %-d %B %Y").to_string(), WHITE),
Span::styled(
match &self.holiday {
Some(h) => format!(" · {}", h.calendar.name),
None => String::new(),
},
color,
),
])];
let mut url = String::new();
if let Some(h) = &self.holiday {
let info = about(h);
lines.push(Line::raw(""));
lines.push(Line::styled(info.blurb, WHITE));
// A subscription's note is where it comes from, not a caveat
if let Some(note) = h
.calendar
.note
.filter(|_| h.calendar.kind != CalKind::Subscribed)
{
lines.push(Line::raw(""));
lines.push(Line::styled(note, SLATE.c400).italic());
}
if !info.url.is_empty() {
lines.push(Line::raw(""));
lines.push(Line::styled(info.url.clone(), CYAN.c300).underlined());
}
url = info.url;
}
lines.push(Line::raw(""));
lines.push(Line::styled("notes and todos", SLATE.c400).bold());
if mine.is_empty() && self.input.is_none() {
lines.push(Line::styled(" none yet", SLATE.c500));
}
let cursor = self.cursor.min(mine.len().saturating_sub(1));
for (i, n) in mine.iter().enumerate() {
let here = i == cursor && self.input.is_none();
let style = match (n.kind.as_str(), n.done) {
("todo", true) => Style::default().fg(SLATE.c500).crossed_out(),
_ => Style::default().fg(WHITE),
};
lines.push(Line::from(vec![
Span::styled(if here { "> " } else { " " }, ORANGE.c400),
Span::styled(notes::line(n), style),
]));
}
if let Some((kind, input)) = &self.input {
let label = match kind {
Kind::Note => "📝 ",
Kind::Todo => "☐ ",
};
lines.push(Line::from(vec![
Span::styled(format!(" {}", label), ORANGE.c400),
Span::styled(input.value().to_string(), WHITE),
Span::styled("▏", ORANGE.c500),
]));
}
lines.push(Line::raw(""));
let help = match (&self.input, url.is_empty()) {
(Some(_), _) => "enter: save · esc: cancel".to_string(),
(None, no_link) => format!(
"n: note · t: todo · space: tick · Delete×2: remove{} · esc: close",
if no_link { "" } else { " · o: open link" }
),
};
lines.push(Line::styled(help, SLATE.c500));
let height = (lines.len() as u16 + 6).clamp(12, 30);
let area = popup_area(f.area(), 76, height);
f.render_widget(Clear, area);
let title = match &self.holiday {
Some(h) => h.name.clone(),
None => self.occ.title.clone(),
};
let popup = Paragraph::new(lines).wrap(Wrap { trim: false }).block(
Block::bordered()
.title(format!(" {} ", title))
.style(Style::default().fg(color)),
);
f.render_widget(popup, area);
}
}
⭐
Log in to mark this repository.
7.6 KiB · text
History
d31eadb