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>,
/// The year the calendar tab shows; scrolling past either end moves it
cal_year: i32,
}
#[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,
}
}
/// Show another year on the calendar tab, with the cursor at `select`
/// (None picks the first date from today on, like opening the tab)
fn set_cal_year(&mut self, year: i32, select: Option<usize>) {
self.cal_year = year;
self.holidays = holidays_in(year, &self.calendars_enabled);
self.sigdate_cal_state = ListState::default();
self.sigdate_cal_state.select(select);
}
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(self.cal_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,
cal_year: Local::now().year(),
}
}
}
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),
},
// Above the first date of the year is the end of the year before
Tabs::Calendar => match app.sigdate_cal_state.selected() {
Some(0) | None => app.set_cal_year(app.cal_year - 1, Some(usize::MAX)),
Some(_) => 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 => match app.sigdate_cal_state.selected() {
Some(i) if i.saturating_add(1) >= app.upcoming_holidays.len() => {
app.set_cal_year(app.cal_year + 1, Some(0))
}
_ => 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(()),
KeyCode::PageUp if app.current_tab == Tabs::Calendar => {
app.set_cal_year(app.cal_year - 1, None)
}
KeyCode::PageDown if app.current_tab == Tabs::Calendar => {
app.set_cal_year(app.cal_year + 1, None)
}
// 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(app.cal_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
// (the first date of a later year, the last of an earlier one)
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(format!("upcoming · {}", app.cal_year)))
.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(
"PgUp/PgDn",
Style::default().add_modifier(Modifier::BOLD).fg(WHITE),
),
Span::raw(" for another year. "),
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)
}
}
⭐
Log in to mark this repository.
83.5 KiB · text
History
1457011