81.8 KiB · text History d9460a8
use crate::data::{
    anniversary_in, delete_editable, CalendarOps, ContactOps, MotdRemindEnum, PplOps, RelationOps,
    SigDateOps, TierDefaultOps, TierOps, TraitDefaultOps, TraitOps,
};
use crate::do_init::lcolor;
use crate::entities::ppl::Model;
use crate::entities::{
    calendar, contact, ppl, relation, sig_date, tier, tier_defaults, trait_defaults, traits,
};
use crate::holidays::{holidays_in, Holiday};
use crate::tui_add::{render_person_menu, AddContext, AddForm, AddKind, FormAction};
use crate::tui_calendars::{
    holiday_color, open_url, render_holiday_info, CalendarMenu, MenuAction,
};
use crate::PplError;
use chrono::{Datelike, Local, NaiveDate};
use crossterm::event;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
use enum_iterator::{next, Sequence};
use itertools::Itertools;
use ratatui::prelude::{Constraint, Direction, Layout, Line, Modifier, Span, Style, Stylize, Text};
use ratatui::style::palette::material::AMBER;
use ratatui::style::palette::tailwind::{GREEN, ORANGE, PINK, SLATE, WHITE};
use ratatui::style::Color;
use ratatui::widgets::calendar::{CalendarEventStore, Monthly};
use ratatui::widgets::{Block, Borders, List, ListDirection, ListItem, ListState, Paragraph};
use ratatui::{DefaultTerminal, Frame};
use sea_orm::DatabaseConnection;
use std::ops::Index;
use time::{Date, Month};
use tui_input::backend::crossterm::EventHandler;
use tui_input::Input;

struct Tui {
    current_tab: Tabs,
    sigdate_list: Vec<sig_date::Model>,
    sigdate_cal_state: ListState,
    trait_list: Vec<traits::Model>,
    tier_list: Vec<tier::Model>,
    rel_list: Vec<relation::Model>,
    contact_list: Vec<contact::Model>,
    ppl_list: Vec<Model>,
    ppl_state: ListState,
    ppl_editing: bool,
    ppl_field_editing: bool,
    ppl_field_idx: u8,
    ppl_editables: Vec<Editable>,
    ppl_input_a: Input,
    ppl_input_b: Input,
    ppl_input_c: Input,
    ppl_detail_state: ListState,
    ppl_show_contacts: bool,
    ppl_show_dates: bool,
    ppl_show_traits: bool,
    ppl_show_tiers: bool,
    ppl_show_relations: bool,
    def_tier_list: Vec<tier_defaults::Model>,
    def_tier_state: ListState,
    def_tier_editing: bool,
    def_tier_input: Input,
    def_trait_list: Vec<trait_defaults::Model>,
    def_trait_state: ListState,
    /// Shown in place of the welcome line until the next key press, e.g. a failed save
    status: Option<String>,
    /// The "add what?" menu for the selected person is open
    add_menu: bool,
    add_form: Option<AddForm>,
    /// The detail row Delete was pressed on once; a second press removes it
    pending_delete: Option<(Editablez, i32)>,
    /// Keys of the built-in holiday calendars that are on
    calendars_enabled: Vec<String>,
    /// This year's holidays from those calendars
    holidays: Vec<Holiday>,
    /// The popup for switching built-in calendars on and off
    calendar_menu: Option<CalendarMenu>,
    /// Calendar rows, for their motd reminder settings
    calendar_rows: Vec<calendar::Model>,
    /// The holiday behind each row of the upcoming list (None for people's dates), for `i`
    upcoming_holidays: Vec<Option<Holiday>>,
    /// The holiday whose blurb is showing
    holiday_info: Option<Holiday>,
}

#[derive(Debug, PartialEq, Sequence, Clone)]
enum Tabs {
    Ppl,
    Calendar,
    TierSettings,
    TraitSettings,
}

#[derive(Clone, Debug, PartialEq)]
pub enum Editablez {
    Ppl,
    Trait,
    Tier,
    Contact,
    Relation,
    SigDate,
}

#[derive(Clone, Debug)]
pub struct Editable {
    pub tgt: Editablez,
    pub id: i32,
    pub first: String,
    pub second: Option<String>,
    pub third: Option<String>,
}

fn key_in_tier(key: String, tiers: &[tier_defaults::Model]) -> bool {
    let t = tiers
        .iter()
        .map(|t| t.key.to_string())
        .collect::<Vec<String>>();
    t.contains(&key)
}

fn get_key_in_tier(key: String, tiers: &[tier_defaults::Model]) -> bool {
    let t = tiers
        .iter()
        .map(|t| t.key.to_string())
        .collect::<Vec<String>>();

    tiers.iter().find(|t| t.key == key);
    t.contains(&key)
}

impl Tui {
    fn add_ctx(&self) -> AddContext<'_> {
        AddContext {
            ppl: &self.ppl_list,
            tiers: &self.tier_list,
            tier_defaults: &self.def_tier_list,
            trait_defaults: &self.def_trait_list,
        }
    }

    fn selected_person(&self) -> Option<&ppl::Model> {
        self.ppl_state
            .selected()
            .and_then(|idx| self.ppl_list.get(idx))
    }

    async fn reload(&mut self, db: &DatabaseConnection) -> Result<(), PplError> {
        self.ppl_list = PplOps::list(db).await?;
        self.def_trait_list = TraitDefaultOps::list(db).await?;
        self.def_tier_list = TierDefaultOps::list(db).await?;
        self.tier_list = TierOps::list(db).await?;
        self.trait_list = TraitOps::list(db).await?;
        self.rel_list = RelationOps::list(db).await?;
        self.contact_list = ContactOps::list(db).await?;
        self.sigdate_list = SigDateOps::list(db).await?;
        self.calendars_enabled = CalendarOps::enabled(db).await?;
        self.calendar_rows = CalendarOps::rows(db).await?;
        self.holidays = holidays_in(Local::now().year(), &self.calendars_enabled);

        Ok(())
    }
}

impl Default for Tui {
    fn default() -> Self {
        Tui {
            current_tab: Tabs::Ppl,
            sigdate_list: vec![],
            sigdate_cal_state: ListState::default(),
            trait_list: vec![],
            tier_list: vec![],
            rel_list: vec![],
            contact_list: vec![],
            ppl_list: vec![],
            ppl_state: ListState::default(),
            ppl_editing: false,
            ppl_field_editing: false,
            ppl_field_idx: 0,
            ppl_editables: vec![],
            ppl_input_a: Input::default(),
            ppl_input_b: Input::default(),
            ppl_input_c: Input::default(),
            ppl_detail_state: ListState::default(),
            ppl_show_contacts: true,
            ppl_show_dates: true,
            ppl_show_traits: true,
            ppl_show_tiers: true,
            ppl_show_relations: true,
            def_tier_list: vec![],
            def_tier_state: ListState::default(),
            def_tier_editing: false,
            def_tier_input: Input::default(),
            def_trait_list: vec![],
            def_trait_state: ListState::default(),
            status: None,
            add_menu: false,
            add_form: None,
            pending_delete: None,
            calendars_enabled: vec![],
            holidays: vec![],
            calendar_menu: None,
            calendar_rows: vec![],
            upcoming_holidays: vec![],
            holiday_info: None,
        }
    }
}

fn get_style(mine: u8, cursor: u8, total: u8) -> Color {
    if cursor % total == mine {
        WHITE
    } else {
        SLATE.c500
    }
}

fn get_cal<'a>(month_start: Date, events: &CalendarEventStore) -> Monthly<'a, &CalendarEventStore> {
    let default_style = Style::default()
        .add_modifier(Modifier::BOLD)
        .fg(AMBER.c500)
        .bg(SLATE.c800);

    Monthly::new(month_start, events)
        .show_month_header(Style::default())
        .default_style(default_style)
}

pub async fn run_tui(
    mut terminal: DefaultTerminal,
    db: DatabaseConnection,
) -> Result<(), PplError> {
    let mut app = Tui::default();
    app.reload(&db).await?;
    loop {
        let _ = terminal.draw(|f| render(f, &mut app));
        let ev = event::read()?;
        let mut confirm_delete = None;
        if matches!(ev, Event::Key(k) if k.kind == KeyEventKind::Press) {
            app.status = None;
            confirm_delete = app.pending_delete.take();
        }
        match ev {
            Event::Key(key_event)
                if key_event.kind == KeyEventKind::Press && app.holiday_info.is_some() =>
            {
                match (key_event.code, &app.holiday_info) {
                    (KeyCode::Char('o'), Some(h)) => {
                        let url = crate::holidays::about(h).url;
                        app.status = Some(match open_url(&url) {
                            Ok(()) => format!("✓ opening {}", url),
                            Err(err) => format!("⚠ couldn't open a browser: {}", err),
                        });
                    }
                    _ => app.holiday_info = None,
                }
            }
            Event::Key(key_event)
                if key_event.kind == KeyEventKind::Press && app.calendar_menu.is_some() =>
            {
                let Some(mut menu) = app.calendar_menu.take() else {
                    continue;
                };
                match menu.handle_key(&key_event, &app.calendars_enabled) {
                    MenuAction::Continue => app.calendar_menu = Some(menu),
                    MenuAction::Close => {}
                    MenuAction::Toggle(c) => {
                        let on = !app.calendars_enabled.iter().any(|k| k == c.key);
                        match CalendarOps::set(&db, c.key, on).await {
                            Ok(changed) => {
                                let names = changed.iter().map(|c| c.name).join(", ");
                                app.status = Some(match on {
                                    true => format!("✓ showing {}", names),
                                    false => format!("✓ hiding {}", names),
                                });
                                app.reload(&db).await?;
                                app.sigdate_cal_state = ListState::default();
                            }
                            Err(err) => app.status = Some(format!("⚠ {}", err)),
                        }
                        app.calendar_menu = Some(menu);
                    }
                    MenuAction::Reminder(target, key) => {
                        let r = target.reminder(&app.calendar_rows);
                        let is_default = matches!(target, crate::data::ReminderTarget::Group(_));
                        if let Some((delta, remind)) = adjust_reminder(
                            key,
                            is_default,
                            (r.delta, r.remind),
                            (r.default_delta, r.default_remind),
                        ) {
                            match CalendarOps::set_reminder(&db, target, delta, remind).await {
                                Ok(()) => app.reload(&db).await?,
                                Err(err) => app.status = Some(format!("⚠ {}", err)),
                            }
                        }
                        app.calendar_menu = Some(menu);
                    }
                }
            }
            Event::Key(key_event) if key_event.kind == KeyEventKind::Press && app.add_menu => {
                app.add_menu = false;
                let kind = AddKind::PERSON_MENU
                    .iter()
                    .find(|(c, _, _)| key_event.code == KeyCode::Char(*c))
                    .map(|(_, kind, _)| *kind);
                if let (Some(kind), Some(p)) = (kind, app.selected_person()) {
                    app.add_form = Some(AddForm::new(kind, Some(p.id), &app.add_ctx()));
                }
            }
            Event::Key(key_event)
                if key_event.kind == KeyEventKind::Press && app.add_form.is_some() =>
            {
                let Some(mut form) = app.add_form.take() else {
                    continue;
                };
                match form.handle_key(&key_event) {
                    FormAction::Continue => app.add_form = Some(form),
                    FormAction::Cancel => {}
                    FormAction::Submit => match form.submit(&db, &app.add_ctx()).await {
                        Ok(msg) => {
                            app.status = Some(format!("✓ {}", msg));
                            app.reload(&db).await?;
                        }
                        // Keep the form open so the input can be fixed
                        Err(err) => {
                            app.status = Some(format!("⚠ {}", err));
                            app.add_form = Some(form);
                        }
                    },
                }
            }
            Event::Key(key_event)
                if key_event.kind == KeyEventKind::Press
                    && app.current_tab == Tabs::TierSettings
                    && app.def_tier_editing =>
            {
                match key_event.code {
                    KeyCode::Esc => app.def_tier_editing = false,
                    KeyCode::Enter => {
                        let value = app.def_tier_input.value().trim();
                        // Empty clears the reminder window, anything else must be a day count
                        let delta = match value.is_empty() {
                            true => Some(None),
                            false => value.parse::<u32>().ok().map(Some),
                        };
                        if let (Some(delta), Some(t)) = (
                            delta,
                            app.def_tier_state
                                .selected()
                                .and_then(|idx| app.def_tier_list.get(idx)),
                        ) {
                            TierDefaultOps::set_sig_date_delta(&db, t.id, delta).await?;
                            app.reload(&db).await?;
                            app.def_tier_editing = false;
                        }
                    }
                    KeyCode::Char(c) if !c.is_ascii_digit() => {}
                    _ => {
                        app.def_tier_input.handle_event(&Event::Key(key_event));
                    }
                }
            }
            Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
                match key_event.code {
                    KeyCode::Up => match app.current_tab {
                        Tabs::Ppl => match app.ppl_editing {
                            true => match app.ppl_field_editing {
                                true => {}
                                false => app.ppl_detail_state.scroll_up_by(1),
                            },

                            false => app.ppl_state.scroll_up_by(1),
                        },
                        Tabs::Calendar => app.sigdate_cal_state.scroll_up_by(1),
                        Tabs::TierSettings => app.def_tier_state.scroll_up_by(1),
                        Tabs::TraitSettings => app.def_trait_state.scroll_up_by(1),
                    },
                    KeyCode::Down => match app.current_tab {
                        Tabs::Ppl => match app.ppl_editing {
                            true => match app.ppl_field_editing {
                                true => {}
                                false => app.ppl_detail_state.scroll_down_by(1),
                            },
                            false => app.ppl_state.scroll_down_by(1),
                        },
                        Tabs::Calendar => app.sigdate_cal_state.scroll_down_by(1),
                        Tabs::TierSettings => app.def_tier_state.scroll_down_by(1),
                        Tabs::TraitSettings => app.def_trait_state.scroll_down_by(1),
                    },
                    KeyCode::Tab => match app.ppl_field_editing {
                        true => {
                            app.ppl_field_idx = app.ppl_field_idx.wrapping_add(1);
                        }
                        false => app.current_tab = next(&app.current_tab).unwrap_or(Tabs::Ppl),
                    },
                    KeyCode::Esc => break Ok(()),
                    // While typing in a field, letters are text, not the section toggles below
                    KeyCode::Char(_) if app.current_tab == Tabs::Ppl && app.ppl_field_editing => {
                        default_editing_handler(&mut app, &key_event);
                    }
                    KeyCode::Char('e') => {
                        if app.current_tab == Tabs::Ppl {
                            match app.ppl_field_editing {
                                true => {
                                    default_editing_handler(&mut app, &key_event);
                                }
                                false => {
                                    app.ppl_editing = !app.ppl_editing;
                                }
                            }
                        }
                    }
                    KeyCode::Char('f') => {
                        default_editing_handler(&mut app, &key_event);
                    }
                    KeyCode::Char('c') | KeyCode::Char('C') => match app.current_tab {
                        Tabs::Ppl => app.ppl_show_contacts = !app.ppl_show_contacts,
                        Tabs::Calendar => app.calendar_menu = Some(CalendarMenu::new()),
                        _ => {}
                    },
                    KeyCode::Char('d') => {
                        if app.current_tab == Tabs::Ppl {
                            app.ppl_show_dates = !app.ppl_show_dates
                        }
                    }
                    KeyCode::Char('r') => {
                        if app.current_tab == Tabs::Ppl {
                            app.ppl_show_traits = !app.ppl_show_traits
                        }
                    }
                    KeyCode::Char('i') => match app.current_tab {
                        Tabs::Ppl => app.ppl_show_tiers = !app.ppl_show_tiers,
                        Tabs::Calendar => {
                            let selected = app
                                .sigdate_cal_state
                                .selected()
                                .and_then(|idx| app.upcoming_holidays.get(idx));
                            match selected {
                                Some(Some(h)) => app.holiday_info = Some(h.clone()),
                                Some(None) => {
                                    app.status =
                                        Some("ℹ i tells you about built-in holidays".to_string())
                                }
                                None => {}
                            }
                        }
                        _ => {}
                    },
                    KeyCode::Char('l') => {
                        if app.current_tab == Tabs::Ppl {
                            app.ppl_show_relations = !app.ppl_show_relations
                        }
                    }
                    KeyCode::Enter if app.current_tab == Tabs::TierSettings => {
                        if let Some(t) = app
                            .def_tier_state
                            .selected()
                            .and_then(|idx| app.def_tier_list.get(idx))
                        {
                            app.def_tier_input = t
                                .sig_date_delta
                                .map(|d| d.to_string())
                                .unwrap_or_default()
                                .into();
                            app.def_tier_editing = true;
                        }
                    }
                    KeyCode::Enter => {
                        if app.current_tab == Tabs::Ppl && app.ppl_editing {
                            let Some(e) = app
                                .ppl_detail_state
                                .selected()
                                .and_then(|idx| app.ppl_editables.get(idx))
                                .cloned()
                            else {
                                continue;
                            };
                            if !app.ppl_field_editing {
                                app.ppl_input_a = e.first.clone().into();
                                app.ppl_input_b = e.second.clone().unwrap_or_default().into();
                                app.ppl_input_c = e.third.clone().unwrap_or_default().into();
                            } else {
                                let new_editable = Editable {
                                    tgt: e.tgt.clone(),
                                    id: e.id,
                                    first: app.ppl_input_a.value().to_string(),
                                    second: Option::from(app.ppl_input_b.value().to_string()),
                                    third: Option::from(app.ppl_input_c.value().to_string()),
                                };

                                let saved = match e.tgt {
                                    Editablez::Ppl => PplOps::updatee(&db, new_editable).await,
                                    Editablez::Trait => TraitOps::updatee(&db, new_editable).await,
                                    Editablez::Tier => TierOps::updatee(&db, new_editable).await,
                                    Editablez::Contact => {
                                        ContactOps::updatee(&db, new_editable).await
                                    }
                                    Editablez::Relation => {
                                        RelationOps::updatee(&db, new_editable).await
                                    }
                                    Editablez::SigDate => {
                                        SigDateOps::updatee(&db, new_editable).await
                                    }
                                };
                                // Keep the fields open so the input can be fixed
                                if let Err(err) = saved {
                                    app.status = Some(format!("⚠ couldn't save: {}", err));
                                    continue;
                                }
                                app.reload(&db).await?;
                            }
                            app.ppl_field_editing = !app.ppl_field_editing;
                        }
                    }
                    KeyCode::Char('a') if !app.ppl_field_editing => match app.current_tab {
                        Tabs::Ppl if app.selected_person().is_some() => app.add_menu = true,
                        Tabs::TierSettings => {
                            app.add_form =
                                Some(AddForm::new(AddKind::DefaultTier, None, &app.add_ctx()))
                        }
                        Tabs::TraitSettings => {
                            app.add_form =
                                Some(AddForm::new(AddKind::DefaultTrait, None, &app.add_ctx()))
                        }
                        _ => {}
                    },
                    KeyCode::Delete | KeyCode::Backspace
                        if app.current_tab == Tabs::Ppl
                            && app.ppl_editing
                            && !app.ppl_field_editing =>
                    {
                        let Some(e) = app
                            .ppl_detail_state
                            .selected()
                            .and_then(|idx| app.ppl_editables.get(idx))
                            .cloned()
                        else {
                            continue;
                        };
                        if confirm_delete == Some((e.tgt.clone(), e.id)) {
                            match delete_editable(&db, &e).await {
                                Ok(()) => {
                                    app.status = Some(format!("✓ removed {}", e.first));
                                    app.reload(&db).await?;
                                }
                                Err(err) => app.status = Some(format!("⚠ {}", err)),
                            }
                        } else if e.tgt == Editablez::Ppl {
                            app.status = Some("⚠ people can't be removed from here".to_string());
                        } else {
                            app.status = Some(format!(
                                "⚠ press Delete again to remove {} {}",
                                e.first,
                                e.second.clone().unwrap_or_default()
                            ));
                            app.pending_delete = Some((e.tgt.clone(), e.id));
                        }
                    }
                    KeyCode::Char(c @ ('m' | '+' | '-' | '[' | ']' | 'x'))
                        if !app.ppl_field_editing =>
                    {
                        tier_reminder_key(&mut app, &db, c).await?;
                    }
                    _ => {
                        default_editing_handler(&mut app, &key_event);
                    }
                }
            }

            _ => {}
        }
    }
}

/// The tier row the reminder keys act on: a tier default on the tiers tab,
/// or the selected tier of a person being edited on the ppl tab
struct ReminderTarget {
    is_default: bool,
    id: i32,
    delta: Option<u32>,
    remind: Option<MotdRemindEnum>,
    /// The tier default a person's tier falls back to
    default_delta: Option<u32>,
    default_remind: Option<MotdRemindEnum>,
}

fn parse_remind(r: &Option<String>) -> Option<MotdRemindEnum> {
    r.as_deref().and_then(|r| r.parse().ok())
}

fn reminder_target(app: &Tui) -> Option<ReminderTarget> {
    match app.current_tab {
        Tabs::TierSettings => app
            .def_tier_state
            .selected()
            .and_then(|idx| app.def_tier_list.get(idx))
            .map(|t| ReminderTarget {
                is_default: true,
                id: t.id,
                delta: t.sig_date_delta,
                remind: parse_remind(&t.sig_remind_enum),
                default_delta: None,
                default_remind: None,
            }),
        Tabs::Ppl if app.ppl_editing => app
            .ppl_detail_state
            .selected()
            .and_then(|idx| app.ppl_editables.get(idx))
            .filter(|e| matches!(e.tgt, Editablez::Tier))
            .and_then(|e| app.tier_list.iter().find(|t| t.id == e.id))
            .map(|t| {
                let d = app.def_tier_list.iter().find(|d| d.key == t.name);
                ReminderTarget {
                    is_default: false,
                    id: t.id,
                    delta: t.sig_date_delta,
                    remind: parse_remind(&t.sig_remind_enum),
                    default_delta: d.and_then(|d| d.sig_date_delta),
                    default_remind: d.and_then(|d| parse_remind(&d.sig_remind_enum)),
                }
            }),
        _ => None,
    }
}

/// m: cycle remind mode, +/-: window days, [/]: random percent, x: clear
async fn tier_reminder_key(
    app: &mut Tui,
    db: &DatabaseConnection,
    key: char,
) -> Result<(), PplError> {
    let Some(t) = reminder_target(app) else {
        return Ok(());
    };
    let Some((new_delta, new_remind)) = adjust_reminder(
        key,
        t.is_default,
        (t.delta, t.remind),
        (t.default_delta, t.default_remind),
    ) else {
        return Ok(());
    };

    if t.is_default {
        TierDefaultOps::set_sig_date_delta(db, t.id, new_delta).await?;
        TierDefaultOps::set_sig_remind_enum(db, t.id, new_remind).await?;
    } else {
        TierOps::set_sig_date_delta(db, t.id, new_delta).await?;
        TierOps::set_sig_remind_enum(db, t.id, new_remind).await?;
    }
    app.reload(db).await
}

/// The reminder keys: `+`/`-` days, `m` cycles the mode, `[`/`]` random %, `x` clears.
/// Returns the new window and mode, or None when the key doesn't apply. A setting with a
/// default to fall back to goes back to it after Never; a default just wraps around.
pub(crate) fn adjust_reminder(
    key: char,
    is_default: bool,
    own: (Option<u32>, Option<MotdRemindEnum>),
    defaults: (Option<u32>, Option<MotdRemindEnum>),
) -> Option<(Option<u32>, Option<MotdRemindEnum>)> {
    let t = ReminderTarget {
        is_default,
        id: 0,
        delta: own.0,
        remind: own.1,
        default_delta: defaults.0,
        default_remind: defaults.1,
    };
    let delta = t.delta.or(t.default_delta);
    let remind = t.remind.or(t.default_remind);

    Some(match key {
        '+' => (Some(delta.unwrap_or(0).saturating_add(1)), t.remind),
        '-' => (Some(delta.unwrap_or(0).saturating_sub(1)), t.remind),
        // A person's tier goes back to the default after Never; a default just wraps around
        'm' => (
            t.delta,
            match (t.is_default, t.remind) {
                (false, Some(MotdRemindEnum::Never)) => None,
                (false, None) => Some(MotdRemindEnum::OnlyOnce),
                (_, r) => Some(r.unwrap_or_default().next()),
            },
        ),
        '[' | ']' => match remind {
            Some(MotdRemindEnum::Randomly(p)) => {
                let p = match key {
                    ']' => p.saturating_add(10).min(100),
                    _ => p.saturating_sub(10),
                };
                (t.delta, Some(MotdRemindEnum::Randomly(p)))
            }
            _ => return None,
        },
        'x' => (None, None),
        _ => return None,
    })
}

/// "⏰ 14d · hourly", with a person's values that come from the tier default dimmed
pub(crate) fn reminder_spans(
    delta: Option<u32>,
    remind: Option<MotdRemindEnum>,
    default_delta: Option<u32>,
    default_remind: Option<MotdRemindEnum>,
) -> Vec<Span<'static>> {
    let style = |own: bool| match own {
        true => Style::default().fg(ORANGE.c400),
        false => Style::default().fg(SLATE.c500),
    };
    let window = match delta.or(default_delta) {
        Some(d) => format!("{}d", d),
        None => "-".to_string(),
    };
    let mode = remind
        .or(default_remind)
        .map(|r| r.label())
        .unwrap_or_else(|| "-".to_string());
    vec![
        Span::styled("  ⏰ ", style(false)),
        Span::styled(window, style(delta.is_some())),
        Span::styled(" · ", style(false)),
        Span::styled(mode, style(remind.is_some())),
    ]
}

fn default_editing_handler(app: &mut Tui, key_event: &KeyEvent) {
    // ppl_editing stays on when switching tabs, so check we're still on the ppl tab
    if app.ppl_editing && app.current_tab == Tabs::Ppl {
        let Some(e) = app
            .ppl_detail_state
            .selected()
            .and_then(|idx| app.ppl_editables.get(idx))
        else {
            return;
        };
        match e.tgt {
            Editablez::Ppl | Editablez::Trait => match app.ppl_field_idx % 2 {
                0 => {
                    app.ppl_input_a.handle_event(&Event::Key(*key_event));
                }
                1 => {
                    app.ppl_input_b.handle_event(&Event::Key(*key_event));
                }
                _ => {}
            },
            Editablez::Tier => match app.ppl_field_idx % 3 {
                0 => {
                    app.ppl_input_a.handle_event(&Event::Key(*key_event));
                }
                1 => {
                    app.ppl_input_b.handle_event(&Event::Key(*key_event));
                }
                2 => {
                    app.ppl_input_c.handle_event(&Event::Key(*key_event));
                }
                _ => {}
            },
            Editablez::Contact => match app.ppl_field_idx % 3 {
                0 => {
                    app.ppl_input_a.handle_event(&Event::Key(*key_event));
                }
                1 => {
                    app.ppl_input_b.handle_event(&Event::Key(*key_event));
                }
                2 => {
                    app.ppl_input_c.handle_event(&Event::Key(*key_event));
                }
                _ => {}
            },
            Editablez::Relation => match app.ppl_field_idx % 3 {
                0 => {
                    app.ppl_input_a.handle_event(&Event::Key(*key_event));
                }
                1 => {
                    app.ppl_input_b.handle_event(&Event::Key(*key_event));
                }
                2 => {
                    app.ppl_input_c.handle_event(&Event::Key(*key_event));
                }
                _ => {}
            },
            Editablez::SigDate => match app.ppl_field_idx % 3 {
                0 => {
                    app.ppl_input_a.handle_event(&Event::Key(*key_event));
                }
                1 => {
                    app.ppl_input_b.handle_event(&Event::Key(*key_event));
                }
                2 => {
                    app.ppl_input_c.handle_event(&Event::Key(*key_event));
                }
                _ => {}
            },
        }
        {}
    }
}

fn render(f: &mut Frame, app: &mut Tui) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(2)
        .constraints(
            [
                Constraint::Length(1),
                Constraint::Percentage(100),
                Constraint::Length(1),
            ]
            .as_ref(),
        )
        .split(f.area());

    let msgw = vec![
        Span::raw("welcome to "),
        Span::styled(
            "ppl",
            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
        ),
        Span::raw(" :)"),
    ];
    let textw = match &app.status {
        Some(status) if status.starts_with('✓') => {
            Text::from(Line::styled(status.clone(), GREEN.c400))
        }
        Some(status) => Text::from(Line::styled(status.clone(), ORANGE.c400)),
        None => Text::from(Line::from(msgw)).style(Style::default()),
    };
    let welcome = Paragraph::new(textw);
    f.render_widget(welcome, chunks[0]);

    match app.current_tab {
        Tabs::Ppl => {
            match app.ppl_editing {
                true => {
                    let mut msgw = vec![
                        Span::styled(
                            "Enter",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ),
                        Span::raw(" to modify a ppl entry. "),
                        Span::styled("a", Style::default().add_modifier(Modifier::BOLD).fg(WHITE)),
                        Span::raw(" to add one. "),
                        Span::styled(
                            "Delete",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ),
                        Span::raw(" twice to remove one"),
                    ];
                    if reminder_target(app).is_some() {
                        msgw.push(Span::styled(
                            " · m mode · +/- days · [/] random% · x use defaults",
                            Style::default().fg(SLATE.c500),
                        ));
                    }
                    let textw = Text::from(Line::from(msgw)).style(Style::default());
                    let welcome = Paragraph::new(textw);
                    f.render_widget(welcome, chunks[2]);
                }
                false => {
                    let mut msgw = vec![
                        Span::styled(
                            "Tab",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ),
                        Span::raw(" to switch views. "),
                        Span::styled(
                            "Enter",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ),
                        Span::raw(" to add ppl. "),
                        Span::styled("E", Style::default().add_modifier(Modifier::BOLD).fg(WHITE)),
                        Span::raw(" to edit the selected ppl. "),
                    ];
                    if app.ppl_show_contacts {
                        msgw.push(Span::styled(
                            "C",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ));
                    } else {
                        msgw.push(Span::styled(
                            "C",
                            Style::default().add_modifier(Modifier::BOLD).fg(SLATE.c500),
                        ));
                    }
                    msgw.push(Span::raw("/"));

                    if app.ppl_show_dates {
                        msgw.push(Span::styled(
                            "D",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ));
                    } else {
                        msgw.push(Span::styled(
                            "D",
                            Style::default().add_modifier(Modifier::BOLD).fg(SLATE.c500),
                        ));
                    }
                    msgw.push(Span::raw("/"));

                    if app.ppl_show_traits {
                        msgw.push(Span::styled(
                            "R",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ));
                    } else {
                        msgw.push(Span::styled(
                            "R",
                            Style::default().add_modifier(Modifier::BOLD).fg(SLATE.c500),
                        ));
                    }
                    msgw.push(Span::raw("/"));

                    if app.ppl_show_tiers {
                        msgw.push(Span::styled(
                            "I",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ));
                    } else {
                        msgw.push(Span::styled(
                            "I",
                            Style::default().add_modifier(Modifier::BOLD).fg(SLATE.c500),
                        ));
                    }
                    msgw.push(Span::raw("/"));

                    if app.ppl_show_relations {
                        msgw.push(Span::styled(
                            "L",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ));
                    } else {
                        msgw.push(Span::styled(
                            "L",
                            Style::default().add_modifier(Modifier::BOLD).fg(SLATE.c500),
                        ));
                    }

                    msgw.push(Span::raw(
                        " to toggle Contacts/Dates/tRaits/tIers/reLations · e edit · a add",
                    ));

                    let textw = Text::from(Line::from(msgw)).style(Style::default());
                    let welcome = Paragraph::new(textw);
                    f.render_widget(welcome, chunks[2]);
                }
            }

            let layout_ppl = Layout::default()
                .direction(Direction::Horizontal)
                .margin(0)
                .constraints([Constraint::Percentage(25), Constraint::Percentage(75)])
                .split(chunks[1]);
            let layout_edit = Layout::default()
                .direction(Direction::Vertical)
                .margin(0)
                .constraints([
                    Constraint::Length(1),
                    Constraint::Length(3),
                    Constraint::Length(3),
                    Constraint::Length(3),
                    Constraint::Length(3),
                ])
                .split(layout_ppl[1]);
            let ppls: Vec<PplAndProps> = app
                .ppl_list
                .iter()
                .map(|p| PplAndProps {
                    ppl: p.clone(),
                    tiers: app
                        .tier_list
                        .iter()
                        .filter(|t| t.ppl_id == p.id)
                        .map(|t| t.to_owned())
                        .collect::<Vec<tier::Model>>(),
                    tier_defaults: app.def_tier_list.clone(),
                })
                .collect::<Vec<PplAndProps>>();

            let ppl = List::new(ppls)
                .block(Block::bordered().title("ppl"))
                .style(Style::new().white())
                .highlight_style(Style::new().italic())
                .highlight_symbol(">>")
                .repeat_highlight_symbol(true)
                .direction(ListDirection::TopToBottom);

            f.render_stateful_widget(&ppl, layout_ppl[0], &mut app.ppl_state);

            let curr = app
                .ppl_state
                .selected()
                .and_then(|idx| app.ppl_list.get(idx))
                .cloned();
            match curr {
                None => {}
                Some(curr) => {
                    let mut msgs: Vec<Line> = vec![];
                    let mut editables: Vec<Editable> = vec![];

                    editables.push(Editable {
                        tgt: Editablez::Ppl,
                        id: curr.id,
                        first: curr.name.trim().to_string(),
                        second: curr.nick.clone(),
                        third: None,
                    });
                    msgs.push(Line::from(vec![
                        Span::styled(
                            "👤 name: ",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ),
                        Span::raw(curr.name.trim().to_string()),
                        Span::styled(
                            "  nick: ",
                            Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                        ),
                        Span::raw(curr.nick.clone().unwrap_or_else(|| "-".to_string())),
                    ]));

                    app.trait_list.sort_by_key(|k| {
                        (
                            key_in_tier(k.key.clone(), &app.def_tier_list),
                            k.key.clone(),
                            k.value.clone(),
                        )
                    });
                    let curr_traits = app
                        .trait_list
                        .iter()
                        .filter(|t| t.ppl_id == curr.id)
                        .map(|t| t.to_owned())
                        .collect::<Vec<traits::Model>>();
                    let trait_defaults = app
                        .def_trait_list
                        .iter()
                        .filter(|t| !t.is_date && !t.is_contact)
                        .collect::<Vec<&trait_defaults::Model>>();
                    if app.ppl_show_traits {
                        for t in &curr_traits {
                            editables.push(Editable {
                                tgt: Editablez::Trait,
                                id: t.id,
                                first: t.key.clone(),
                                second: Option::from(t.value.clone()),
                                third: t.hidden.clone().to_string().into(),
                            });
                            match trait_defaults.iter().find(|f| f.key == t.key) {
                                None => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("ℹ️ {}: ", t.key.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.value.clone()),
                                ])),
                                Some(td) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("{} {}: ", td.symbol, t.key.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.value.clone()),
                                ])),
                            }
                        }
                    }

                    let curr_tiers = app
                        .tier_list
                        .iter()
                        .filter(|t| t.ppl_id == curr.id)
                        .map(|t| t.to_owned())
                        .collect::<Vec<tier::Model>>();

                    if app.ppl_show_tiers {
                        for t in &curr_tiers {
                            let othersym = app.def_tier_list.iter().find(|dt| dt.key == t.name);
                            editables.push(Editable {
                                id: t.id,
                                tgt: Editablez::Tier,
                                first: t.name.clone(),
                                second: t.color.clone(),
                                third: t.symbol.clone(),
                            });
                            match (&t.symbol, othersym) {
                                (None, Some(other)) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!(
                                            "{} {}: ",
                                            &other.symbol.clone().unwrap_or_default(),
                                            t.name.clone()
                                        ),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.name.clone()),
                                ])),
                                (Some(main), None) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("{} {}: ", main, t.name.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.name.clone()),
                                ])),
                                (None, None) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("ℹ️ {}: ", t.name.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.name.clone()),
                                ])),
                                // _ => {}
                                (Some(main), Some(_other)) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("{} {}: ", main, t.name.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.name.clone()),
                                ])),
                            }
                            if let Some(line) = msgs.last_mut() {
                                for span in reminder_spans(
                                    t.sig_date_delta,
                                    parse_remind(&t.sig_remind_enum),
                                    othersym.and_then(|d| d.sig_date_delta),
                                    othersym.and_then(|d| parse_remind(&d.sig_remind_enum)),
                                ) {
                                    line.push_span(span);
                                }
                            }
                        }
                    }

                    let curr_rels = app
                        .rel_list
                        .iter()
                        .filter(|t| t.ppl_id_b == curr.id)
                        .map(|t| t.to_owned())
                        .collect::<Vec<relation::Model>>();
                    if app.ppl_show_relations {
                        for t in &curr_rels {
                            let i2 = t.date_entered.map(|d| d.to_string());
                            let i3 = t.date_ended.map(|d| d.to_string());
                            editables.push(Editable {
                                id: t.id,
                                tgt: Editablez::Relation,
                                first: t.r#type.clone(),
                                second: i2,
                                third: i3,
                            });

                            msgs.push(Line::from(vec![
                                Span::styled(
                                    format!("🫂 {}: ", t.r#type.clone()),
                                    Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                ),
                                Span::raw(format!("{:?} - {:?}", t.date_entered, t.date_ended)),
                            ]))
                        }
                    }

                    let curr_dates = app
                        .sigdate_list
                        .iter()
                        .filter(|t| t.ppl_id == curr.id)
                        .map(|t| t.to_owned())
                        .collect::<Vec<sig_date::Model>>();
                    let date_traits = app
                        .def_trait_list
                        .iter()
                        .filter(|t| t.is_date && t.enabled)
                        .collect::<Vec<&trait_defaults::Model>>();
                    if app.ppl_show_dates {
                        for t in &curr_dates {
                            editables.push(Editable {
                                id: t.id,
                                tgt: Editablez::SigDate,
                                first: t.event.clone(),
                                second: Option::from(t.date.to_string()),
                                third: Option::from(t.do_remind.to_string()),
                            });
                            match (date_traits.iter().find(|f| f.key == t.event), t.do_remind) {
                                (Some(dt), _) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("{} {}: ", dt.symbol, t.event.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.date.clone().to_string()),
                                ])),
                                (None, true) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("⏰ {}: ", t.event.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.date.clone().to_string()),
                                ])),
                                (None, false) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("📅 {}: ", t.event.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.date.clone().to_string()),
                                ])),
                            }
                        }
                    }

                    let contacts = app
                        .contact_list
                        .iter()
                        .filter(|t| t.ppl_id == curr.id)
                        .map(|t| t.to_owned())
                        .collect::<Vec<contact::Model>>();
                    let contact_traits = app
                        .def_trait_list
                        .iter()
                        .filter(|t| t.is_contact && t.enabled)
                        .collect::<Vec<&trait_defaults::Model>>();
                    if app.ppl_show_contacts {
                        for t in &contacts {
                            editables.push(Editable {
                                id: t.id,
                                tgt: Editablez::Contact,
                                first: t.r#type.clone(),
                                second: Option::from(
                                    t.designator.clone().unwrap_or("".to_string()),
                                ),
                                third: Option::from(t.value.clone()),
                            });

                            match contact_traits.iter().find(|f| f.key == t.r#type) {
                                Some(dt) => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("{} {}: ", dt.symbol, t.r#type.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.value.clone().to_string()),
                                ])),
                                None => msgs.push(Line::from(vec![
                                    Span::styled(
                                        format!("📇 {}: ", t.r#type.clone()),
                                        Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                                    ),
                                    Span::raw(t.value.clone().to_string()),
                                ])),
                            }
                        }
                    }

                    app.ppl_editables = editables.clone();
                    let list_contact = List::new(msgs.clone())
                        .block(Block::bordered().title("Circles"))
                        .style(Style::new().white())
                        .highlight_style(Style::new().italic())
                        .highlight_symbol(">>")
                        .repeat_highlight_symbol(true)
                        .direction(ListDirection::TopToBottom);

                    let textw = Text::from(msgs.clone()).style(Style::default());
                    let paragraph = Paragraph::new(textw);
                    match app.ppl_editing {
                        true => match app.ppl_field_editing {
                            true => {
                                let Some(ced) = app
                                    .ppl_detail_state
                                    .selected()
                                    .and_then(|idx| editables.get(idx))
                                else {
                                    return;
                                };
                                match ced.tgt.clone() {
                                    Editablez::Ppl => {
                                        let input_a = Paragraph::new(app.ppl_input_a.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("name"),
                                            )
                                            .style(get_style(0, app.ppl_field_idx, 2));
                                        f.render_widget(input_a, layout_edit[1]);

                                        let input_b = Paragraph::new(app.ppl_input_b.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("nick (empty clears)"),
                                            )
                                            .style(get_style(1, app.ppl_field_idx, 2));
                                        f.render_widget(input_b, layout_edit[2]);
                                    }
                                    Editablez::Trait => {
                                        let input_a = Paragraph::new(app.ppl_input_a.value())
                                            .block(
                                                Block::default().borders(Borders::ALL).title("key"),
                                            )
                                            .style(get_style(0, app.ppl_field_idx, 2));
                                        f.render_widget(input_a, layout_edit[1]);

                                        let input_b = Paragraph::new(app.ppl_input_b.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("value"),
                                            )
                                            .style(get_style(1, app.ppl_field_idx, 2));
                                        f.render_widget(input_b, layout_edit[2]);
                                    }
                                    Editablez::Tier => {
                                        let input_b = Paragraph::new(app.ppl_input_a.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("name"),
                                            )
                                            .style(get_style(0, app.ppl_field_idx, 3));
                                        f.render_widget(input_b, layout_edit[1]);
                                        let input_b = Paragraph::new(app.ppl_input_b.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("color"),
                                            )
                                            .style(get_style(1, app.ppl_field_idx, 3));
                                        f.render_widget(input_b, layout_edit[2]);

                                        let input_c = Paragraph::new(app.ppl_input_c.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("symbol"),
                                            )
                                            .style(get_style(2, app.ppl_field_idx, 3));
                                        f.render_widget(input_c, layout_edit[3]);
                                    }
                                    Editablez::Contact => {
                                        let input_a = Paragraph::new(app.ppl_input_a.value())
                                            .block(
                                                Block::default().borders(Borders::ALL).title("typ"),
                                            )
                                            .style(get_style(0, app.ppl_field_idx, 3));
                                        f.render_widget(input_a, layout_edit[1]);

                                        let input_b = Paragraph::new(app.ppl_input_b.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("designator"),
                                            )
                                            .style(get_style(1, app.ppl_field_idx, 3));
                                        f.render_widget(input_b, layout_edit[2]);
                                        let input_c = Paragraph::new(app.ppl_input_c.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("value"),
                                            )
                                            .style(get_style(2, app.ppl_field_idx, 3));
                                        f.render_widget(input_c, layout_edit[3]);
                                    }
                                    Editablez::Relation => {
                                        let input_a = Paragraph::new(app.ppl_input_a.value())
                                            .block(
                                                Block::default().borders(Borders::ALL).title("key"),
                                            )
                                            .style(get_style(0, app.ppl_field_idx, 3));
                                        f.render_widget(input_a, layout_edit[1]);

                                        let input_b = Paragraph::new(app.ppl_input_b.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("start"),
                                            )
                                            .style(get_style(1, app.ppl_field_idx, 3));
                                        f.render_widget(input_b, layout_edit[2]);

                                        let input_c = Paragraph::new(app.ppl_input_c.value())
                                            .block(
                                                Block::default().borders(Borders::ALL).title("end"),
                                            )
                                            .style(get_style(2, app.ppl_field_idx, 3));
                                        f.render_widget(input_c, layout_edit[3]);
                                    }
                                    Editablez::SigDate => {
                                        let input_a = Paragraph::new(app.ppl_input_a.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("date"),
                                            )
                                            .style(get_style(0, app.ppl_field_idx, 3));
                                        f.render_widget(input_a, layout_edit[1]);

                                        let input_b = Paragraph::new(app.ppl_input_b.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("date"),
                                            )
                                            .style(get_style(1, app.ppl_field_idx, 3));
                                        f.render_widget(input_b, layout_edit[2]);

                                        let input_c = Paragraph::new(app.ppl_input_c.value())
                                            .block(
                                                Block::default()
                                                    .borders(Borders::ALL)
                                                    .title("do_remind"),
                                            )
                                            .style(get_style(2, app.ppl_field_idx, 3));
                                        f.render_widget(input_c, layout_edit[3]);
                                    }
                                }
                                let title = match &curr.nick {
                                    None => {
                                        format!("Editing {}", curr.name.clone())
                                    }
                                    Some(n) => {
                                        format!("Editing {} ({})", curr.name.clone(), n)
                                    }
                                };
                                let block = Block::new()
                                    .borders(Borders::LEFT)
                                    .title(title)
                                    .style(Style::default().fg(ORANGE.c500));
                                f.render_widget(paragraph.clone().block(block), layout_edit[0])
                            }
                            false => {
                                let title = match &curr.nick {
                                    None => {
                                        format!("Editing {}", curr.name.clone())
                                    }
                                    Some(n) => {
                                        format!("Editing {} ({})", curr.name.clone(), n)
                                    }
                                };
                                let block = Block::new()
                                    .borders(Borders::LEFT)
                                    .title(title)
                                    .style(Style::default().fg(AMBER.c500));
                                f.render_stateful_widget(
                                    list_contact.block(block),
                                    layout_ppl[1],
                                    &mut app.ppl_detail_state,
                                );
                            }
                        },
                        false => {
                            let title = match &curr.nick {
                                None => curr.name.clone().to_string(),
                                Some(n) => {
                                    format!("{} ({})", curr.name.clone(), n)
                                }
                            };
                            let block = Block::new().borders(Borders::ALL).title(title);
                            f.render_widget(paragraph.clone().block(block), layout_ppl[1]);
                        }
                    };
                }
            }
        }
        Tabs::Calendar => {
            let layout_cal = Layout::default()
                .direction(Direction::Horizontal)
                .margin(0)
                .constraints([Constraint::Percentage(26), Constraint::Percentage(74)])
                .split(chunks[1]);

            // Build the year start from chrono; time's now_local() can fail depending on platform and threads
            let Ok(mut start) = Date::from_calendar_date(Local::now().year(), Month::January, 1)
            else {
                return;
            };
            let as_upcoming = &app
                .sigdate_list
                .iter()
                .filter_map(|d| {
                    let mut n = d.clone();
                    n.date = anniversary_in(n.date, start.year())?;
                    // Skip dates whose person is gone
                    let p = app.ppl_list.iter().find(|p| p.id == n.ppl_id)?.clone();
                    let tr = app
                        .trait_list
                        .iter()
                        .filter(|t| t.ppl_id == n.ppl_id)
                        .map(|t| t.to_owned())
                        .collect::<Vec<traits::Model>>();
                    Some(crate::do_tui::SigDateAndProps {
                        date: n,
                        ppl: p,
                        traits: tr,
                        trait_defaults: app.def_trait_list.clone(),
                    })
                })
                .sorted_by(|a, b| a.date.date.cmp(&b.date.date))
                .collect::<Vec<SigDateAndProps>>();
            let mut event_list = CalendarEventStore::today(
                Style::default()
                    .add_modifier(Modifier::BOLD)
                    .bg(Color::Blue),
            );

            // Holidays first, so a person's date on the same day wins the colour
            for h in &app.holidays {
                let Ok(day) = Month::try_from(h.date.month() as u8).and_then(|month| {
                    Date::from_calendar_date(h.date.year(), month, h.date.day() as u8)
                }) else {
                    continue;
                };
                event_list.add(
                    day,
                    Style::default()
                        .add_modifier(Modifier::BOLD)
                        .fg(SLATE.c900)
                        .bg(holiday_color(h)),
                );
            }

            for u in as_upcoming.iter().filter(|d| d.date.do_remind) {
                let Ok(day) = Month::try_from(u.date.date.month() as u8).and_then(|month| {
                    Date::from_calendar_date(start.year(), month, u.date.date.day() as u8)
                }) else {
                    continue;
                };
                event_list.add(
                    day,
                    Style::default()
                        .add_modifier(Modifier::BOLD)
                        .fg(GREEN.c300)
                        .bg(PINK.c500),
                );
            }

            // People's dates and holidays together, in date order
            let mut upcoming = as_upcoming
                .iter()
                .map(|u| (u.date.date, None, ListItem::from(u)))
                .collect::<Vec<_>>();
            upcoming.extend(
                app.holidays
                    .iter()
                    .map(|h| (h.date, Some(h.clone()), holiday_item(h))),
            );
            upcoming.sort_by_key(|(d, _, _)| *d);
            // Open on the first date from today on, with a couple of past ones above it
            if app.sigdate_cal_state.selected().is_none() && !upcoming.is_empty() {
                let today = Local::now().date_naive();
                let idx = upcoming
                    .iter()
                    .position(|(d, _, _)| *d >= today)
                    .unwrap_or(upcoming.len() - 1);
                app.sigdate_cal_state.select(Some(idx));
                *app.sigdate_cal_state.offset_mut() = idx.saturating_sub(2);
            }
            // Scrolling can run past the end; keep the selection on a real row
            if let Some(idx) = app.sigdate_cal_state.selected() {
                if idx >= upcoming.len() && !upcoming.is_empty() {
                    app.sigdate_cal_state.select(Some(upcoming.len() - 1));
                }
            }
            // Remember which rows are holidays, for `i`
            app.upcoming_holidays = upcoming.iter().map(|(_, h, _)| h.clone()).collect();

            // The selected row's day stands out in the grid, so scrolling shows where you are
            let selected_day = app
                .sigdate_cal_state
                .selected()
                .and_then(|idx| upcoming.get(idx))
                .and_then(|(d, _, _)| {
                    let month = Month::try_from(d.month() as u8).ok()?;
                    Date::from_calendar_date(d.year(), month, d.day() as u8).ok()
                });
            if let Some(day) = selected_day {
                event_list.add(
                    day,
                    Style::default()
                        .add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
                        .fg(Color::Black)
                        .bg(WHITE),
                );
            }

            // let _ = as_upcoming.iter().map(|u| {
            //     let _ = event_list.add(
            //         Date::from_calendar_date(start.year(), Month::try_from(4 as u8).expect("oops"), 15 as u8).expect("doubleoops"),
            //         // Date::from_calendar_date(start.year(), Month::try_from(u.date.month() as u8).expect("oops"), u.date.day() as u8).expect("doubleoops"),
            //         Style::default()
            //             .add_modifier(Modifier::BOLD)
            //             .fg(GREEN.c300)
            //             .bg(PINK.c500),
            //     );
            // });

            let rows = Layout::vertical([Constraint::Ratio(1, 2); 2]).split(layout_cal[1]);
            let cols = rows.iter().flat_map(|row| {
                Layout::horizontal([Constraint::Ratio(1, 6); 6])
                    .split(*row)
                    .to_vec()
            });
            for col in cols {
                let cal = get_cal(start, &event_list);
                f.render_widget(cal, col);
                start = start.replace_month(start.month().next()).unwrap_or(start);
            }

            let dates = List::new(upcoming.into_iter().map(|(_, _, item)| item))
                .block(Block::bordered().title("upcoming"))
                .style(Style::new().white())
                .highlight_style(Style::new().italic().bold())
                .highlight_symbol(">>")
                .repeat_highlight_symbol(true)
                .direction(ListDirection::TopToBottom);

            f.render_stateful_widget(dates, layout_cal[0], &mut app.sigdate_cal_state);

            // let layout_cal_detail = Layout::default()
            //     .direction(Direction::Horizontal)
            //     .margin(0)
            //     .constraints([Constraint::Percentage(25), Constraint::Percentage(75)])
            //     .split(chunks[1]);

            let msgw = vec![
                Span::styled(
                    "Tab",
                    Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                ),
                Span::raw(" to switch views. "),
                Span::styled("E", Style::default().add_modifier(Modifier::BOLD).fg(WHITE)),
                Span::raw(" to edit the enabled tiers. "),
                Span::styled("C", Style::default().add_modifier(Modifier::BOLD).fg(WHITE)),
                Span::raw(format!(
                    " to choose built-in holiday calendars ({} on). ",
                    app.calendars_enabled.len()
                )),
                Span::styled(
                    "↑/↓",
                    Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
                ),
                Span::raw(" to scroll upcoming. "),
                Span::styled("i", Style::default().add_modifier(Modifier::BOLD).fg(WHITE)),
                Span::raw(" about a holiday. "),
            ];
            let textw = Text::from(Line::from(msgw)).style(Style::default());
            let welcome = Paragraph::new(textw);
            f.render_widget(welcome, chunks[2]);
        }
        Tabs::TierSettings => {
            let ppl = List::new(&app.def_tier_list)
                .block(Block::bordered().title("tiers"))
                .style(Style::new().white())
                .highlight_style(Style::new().italic())
                .highlight_symbol(">>")
                .repeat_highlight_symbol(true)
                .direction(ListDirection::TopToBottom);

            f.render_stateful_widget(ppl, chunks[1], &mut app.def_tier_state);

            let footer = match app.def_tier_editing {
                true => Line::from(vec![
                    Span::raw("remind days ahead: "),
                    Span::styled(app.def_tier_input.value(), Style::default().fg(WHITE)),
                    Span::styled("▏", Style::default().fg(ORANGE.c500)),
                    Span::styled(
                        "  enter save · empty clears · esc cancel",
                        Style::default().fg(SLATE.c500),
                    ),
                ]),
                false => Line::styled(
                    "a: new tier · enter/+/-: days · m: mode · [/]: random% · x: clear · tab: next tab · esc: quit",
                    SLATE.c500,
                ),
            };
            f.render_widget(Paragraph::new(footer), chunks[2]);
        }
        Tabs::TraitSettings => {
            let ppl = List::new(&app.def_trait_list)
                .block(Block::bordered().title("traits"))
                .style(Style::new().white())
                .highlight_style(Style::new().italic())
                .highlight_symbol(">>")
                .repeat_highlight_symbol(true)
                .direction(ListDirection::TopToBottom);

            f.render_stateful_widget(ppl, chunks[1], &mut app.def_trait_state);
            f.render_widget(
                Paragraph::new(Line::styled(
                    "a: new trait · tab: next tab · esc: quit",
                    SLATE.c500,
                )),
                chunks[2],
            );
        }
    }

    // Popups go on top of whichever tab is showing
    let person = app
        .selected_person()
        .map(|p| format!(" {}", p.nick.as_deref().unwrap_or(p.name.trim())))
        .unwrap_or_default();
    if app.add_menu {
        render_person_menu(f, &person);
    }
    if let Some(menu) = app.calendar_menu.as_mut() {
        menu.render(f, &app.calendars_enabled, &app.calendar_rows);
    }
    if let Some(h) = &app.holiday_info {
        render_holiday_info(f, h);
    }
    if let Some(form) = &app.add_form {
        let subtitle = match form.ppl_id {
            Some(_) => format!(" ·{}", person),
            None => String::new(),
        };
        form.render(f, &subtitle);
    }
}

struct PplAndProps {
    ppl: ppl::Model,
    tier_defaults: Vec<tier_defaults::Model>,
    tiers: Vec<tier::Model>,
}

struct SigDateAndProps {
    date: sig_date::Model,
    ppl: ppl::Model,
    traits: Vec<traits::Model>,
    trait_defaults: Vec<trait_defaults::Model>,
}

impl From<PplAndProps> for ListItem<'_> {
    fn from(value: PplAndProps) -> Self {
        // value.date_up;
        let line =
            match value.ppl.me {
                true => Line::styled(
                    format!(" 🌟 {}", value.ppl.nick.unwrap_or(value.ppl.name)),
                    WHITE,
                ),
                false => match value.tiers.len() {
                    0 => Line::styled(
                        format!("   {}", value.ppl.nick.unwrap_or(value.ppl.name)),
                        WHITE,
                    ),
                    _ => {
                        let t = &value.tiers[0];
                        match value.tier_defaults.iter().find(|q| q.key == t.name) {
                            None => Line::styled(
                                format!("   {}", value.ppl.nick.unwrap_or(value.ppl.name)),
                                WHITE,
                            ),
                            Some(tierd) => {
                                match t.symbol.clone().unwrap_or("".to_string()).as_str() {
                                    "" => Line::styled(
                                        format!(
                                            " {} {}",
                                            tierd.symbol.clone().unwrap_or("".to_string()),
                                            value.ppl.nick.unwrap_or(value.ppl.name)
                                        ),
                                        lcolor(&t.color.clone().unwrap_or(
                                            tierd.color.clone().unwrap_or("".to_string()),
                                        )),
                                    ),
                                    otherwise => Line::styled(
                                        format!(
                                            " {} {}",
                                            otherwise,
                                            value.ppl.nick.unwrap_or(value.ppl.name)
                                        ),
                                        lcolor(&t.color.clone().unwrap_or(
                                            tierd.color.clone().unwrap_or("".to_string()),
                                        )),
                                    ),
                                }
                            }
                        }
                    }
                },
            };
        ListItem::new(line)
    }
}

impl From<&SigDateAndProps> for ListItem<'_> {
    fn from(value: &SigDateAndProps) -> Self {
        let n = value.ppl.nick.clone().unwrap_or(value.ppl.name.clone());
        let default = value
            .trait_defaults
            .iter()
            .find(|q| q.key == value.date.event && q.is_date);
        let line = match (value.date.do_remind, default) {
            (true, Some(t)) => Line::styled(
                format!(" ✓ {} {:6} {}", t.symbol, n, value.date.date),
                WHITE,
            ),
            (true, None) => Line::styled(
                format!(" ✓ 📅 {:6} {} ({})", n, value.date.date, value.date.event),
                WHITE,
            ),
            (false, Some(t)) => Line::styled(
                format!(" ☐ {} {:6} {}", t.symbol, n, value.date.date),
                SLATE.c500,
            ),

            (false, None) => Line::styled(
                format!(" ☐ 📅 {:6} {} ({})", n, value.date.date, value.date.event),
                SLATE.c500,
            ),
        };

        ListItem::new(line)
    }
}

fn holiday_item(h: &Holiday) -> ListItem<'static> {
    ListItem::new(Line::from(vec![
        Span::styled(" ◆ ", holiday_color(h)),
        Span::styled(format!("{} {}", h.date, h.name), WHITE),
        Span::styled(format!(" {}", h.calendar.key), SLATE.c500),
    ]))
}

impl From<&ppl::Model> for ListItem<'_> {
    fn from(value: &Model) -> Self {
        let line = match value.me {
            true => Line::styled(format!(" 🌟 {}", value.name), WHITE),
            false => Line::styled(format!("   {}", value.name), WHITE),
        };
        ListItem::new(line)
    }
}

impl From<&tier_defaults::Model> for ListItem<'_> {
    fn from(value: &tier_defaults::Model) -> Self {
        let line = match value.enabled {
            true => Line::styled(
                format!(" ✓ {:?} {}", value.symbol, value.key),
                lcolor(&value.color.clone().unwrap_or("".to_string())),
            ),
            false => Line::styled(format!(" ☐ {:?} {}", value.symbol, value.key), SLATE.c500),
        };
        let mut line = line;
        for span in reminder_spans(
            value.sig_date_delta,
            parse_remind(&value.sig_remind_enum),
            None,
            None,
        ) {
            line.push_span(span);
        }

        ListItem::new(line)
    }
}
impl From<&trait_defaults::Model> for ListItem<'_> {
    fn from(value: &trait_defaults::Model) -> Self {
        let line = match value.enabled {
            true => Line::styled(
                format!(" ✓ {} {}", value.symbol, value.key),
                lcolor(&value.color),
            ),
            false => Line::styled(format!(" ☐ {} {}", value.symbol, value.key), SLATE.c500),
        };

        ListItem::new(line)
    }
}

impl From<&sig_date::Model> for ListItem<'_> {
    fn from(value: &sig_date::Model) -> Self {
        let line = match value.do_remind {
            true => Line::styled(
                format!(" ✓ {} {} {}", value.ppl_id, value.event, value.date),
                WHITE,
            ),
            false => Line::styled(
                format!(" ☐ {} {} {}", value.ppl_id, value.event, value.date),
                SLATE.c500,
            ),
        };

        ListItem::new(line)
    }
}