use std::{borrow::Cow, fmt};
use aimcal_core::{Event, LooseDateTime, RangePosition};
use colored::Color;
use jiff::{Zoned, civil::Date};
use crate::table::{PaddingDirection, Table, TableColumn, TableStyleBasic, TableStyleJson};
use crate::util::{OutputFormat, format_datetime};
#[derive(Debug, Clone)]
pub struct EventFormatter {
now: Zoned,
columns: Vec<EventColumn>,
format: OutputFormat,
}
impl EventFormatter {
pub fn new(now: Zoned, columns: Vec<EventColumn>, format: OutputFormat) -> Self {
Self {
now,
columns,
format,
}
}
pub fn format<'a, E: Event>(&'a self, events: &'a [E]) -> Display<'a, E> {
Display {
events,
formatter: self,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Display<'a, E: Event> {
events: &'a [E],
formatter: &'a EventFormatter,
}
impl<E: Event> fmt::Display for Display<'_, E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let columns: Vec<_> = self
.formatter
.columns
.iter()
.map(|column| ColumnMeta {
column,
now: self.formatter.now.clone(),
})
.collect();
match self.formatter.format {
OutputFormat::Json => {
let table = Table::new(TableStyleJson::new(), &columns, self.events);
write!(f, "{table}")
}
OutputFormat::Table => {
let table = Table::new(TableStyleBasic::new(), &columns, self.events);
write!(f, "{table}")
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum EventColumn {
DateTimeSpan,
Id,
ShortId,
Summary,
TimeSpan { date: Date },
Uid,
}
#[derive(Debug, Clone)]
struct ColumnMeta<'a> {
column: &'a EventColumn,
now: Zoned,
}
impl<E: Event> TableColumn<E> for ColumnMeta<'_> {
fn name(&self) -> Cow<'_, str> {
match self.column {
EventColumn::DateTimeSpan => "Date Time",
EventColumn::Id => "ID",
EventColumn::ShortId => "Short ID",
EventColumn::Summary => "Summary",
EventColumn::TimeSpan { date: _ } => "Time",
EventColumn::Uid => "UID",
}
.into()
}
fn format<'b>(&self, data: &'b E) -> Cow<'b, str> {
match self.column {
EventColumn::DateTimeSpan => format_datetime_span(data),
EventColumn::Id => format_id(data),
EventColumn::ShortId => format_short_id(data),
EventColumn::Summary => format_summary(data),
EventColumn::TimeSpan { date } => format_time_span(data, *date),
EventColumn::Uid => format_uid(data),
}
}
fn padding_direction(&self) -> PaddingDirection {
match self.column {
EventColumn::Id | EventColumn::Uid => PaddingDirection::Right,
_ => PaddingDirection::Left,
}
}
fn get_color(&self, data: &E) -> Option<Color> {
match &self.column {
EventColumn::DateTimeSpan => get_color_datetime_span(data, &self.now),
EventColumn::TimeSpan { date: _ } => get_color_time_span(data, &self.now),
_ => None,
}
}
}
fn format_id(event: &impl Event) -> Cow<'_, str> {
if let Some(short_id) = event.short_id() {
short_id.to_string().into()
} else {
let uid = event.uid(); tracing::warn!(
uid = uid.as_ref(),
"event does not have a short ID, using UID instead.",
);
uid
}
}
fn format_short_id(event: &impl Event) -> Cow<'_, str> {
event
.short_id()
.map(|a| a.to_string())
.unwrap_or_default()
.into()
}
fn format_summary(event: &impl Event) -> Cow<'_, str> {
event.summary().replace('\n', "↵").into()
}
fn format_datetime_span(event: &impl Event) -> Cow<'_, str> {
match (event.start(), event.end()) {
(Some(start), Some(end)) => {
if start.date() == end.date() {
match (start.time(), end.time()) {
(Some(stime), Some(etime)) => format!(
"{} {}~{}",
start.date().strftime("%Y-%m-%d"),
stime.strftime("%H:%M"),
etime.strftime("%H:%M")
),
(Some(stime), None) => format!(
"{} {}~24:00",
start.date().strftime("%Y-%m-%d"),
stime.strftime("%H:%M")
),
(None, Some(etime)) => format!(
"{} 00:00~{}",
start.date().strftime("%Y-%m-%d"),
etime.strftime("%H:%M")
),
(None, None) => start.date().strftime("%Y-%m-%d").to_string(),
}
} else {
format!("{}~{}", format_datetime(start), format_datetime(end))
}
}
.into(),
(Some(start), None) => format_datetime(start).into(),
(None, Some(end)) => format!("~{}", format_datetime(end)).into(),
(None, None) => String::new().into(),
}
}
fn get_color_datetime_span(event: &impl Event, now: &Zoned) -> Option<Color> {
const COLOR_CURRENT: Option<Color> = Some(Color::Yellow);
const COLOR_TODAY_LATE: Option<Color> = Some(Color::Green);
let start = event.start()?; match LooseDateTime::position_in_range(&now.datetime(), &Some(start), &event.end()) {
RangePosition::Before => COLOR_TODAY_LATE,
RangePosition::InRange => COLOR_CURRENT,
RangePosition::After => None,
RangePosition::InvalidRange => {
tracing::warn!(uid = &*event.uid(), "invalid range for event");
None
}
}
}
fn format_time_span(event: &impl Event, date: Date) -> Cow<'_, str> {
fn format_date(d: Date) -> String {
d.strftime("%Y-%m-%d").to_string()
}
match (event.start(), event.end()) {
(Some(start), Some(end)) => {
let sdate = start.date();
let edate = end.date();
if edate < sdate {
String::new() } else if edate < date {
format!("⇥{}", format_date(edate)).to_string() } else if sdate > date {
format!("{}↦", format_date(sdate)).to_string() } else if sdate == date && sdate == edate {
match (start.time(), end.time()) {
(Some(stime), Some(etime)) => {
format!("{}~{}", stime.strftime("%H:%M"), etime.strftime("%H:%M"))
}
(Some(stime), None) => format!("{}⇥", stime.strftime("%H:%M")),
(None, Some(etime)) => format!(" ↦{}", etime.strftime("%H:%M")),
(None, None) => format!("⇹{}", format_date(date)),
}
} else if sdate == date
&& sdate < edate
&& let Some(stime) = start.time()
{
format!("{}↦", stime.strftime("%H:%M")).to_string()
} else if edate == date
&& let Some(etime) = end.time()
{
format!("⇥{}", etime.strftime("%H:%M"))
} else if sdate.year() == date.year() && edate.year() == date.year() {
format!("{}~{}", sdate.strftime("%m-%d"), edate.strftime("%m-%d")).to_string()
} else {
format!("⇸{}", format_date(edate)).to_string()
}
}
.into(),
(Some(start), None) => format_datetime(start).into(),
(None, Some(end)) => format!("↦{}", format_datetime(end)).into(),
(None, None) => String::new().into(),
}
}
fn get_color_time_span(event: &impl Event, now: &Zoned) -> Option<Color> {
get_color_datetime_span(event, now)
}
fn format_uid(event: &impl Event) -> Cow<'_, str> {
event.uid()
}