7.6 KiB · text History aea77cc
//! 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);
    }
}