dyd 1.7.0

CLI for daily diffing of git repos
Documentation
use chrono::{DateTime, Datelike, Duration, NaiveDate, Timelike, Utc, Weekday};
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, StatefulWidget, Widget};

#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Calendar<'a> {
  block: Option<Block<'a>>,
  style: Style,
}

impl<'a> Calendar<'a> {
  pub fn new() -> Self {
    Self {
      block: None,
      style: Default::default(),
    }
  }

  pub fn block(mut self, block: Block<'a>) -> Self {
    self.block = Some(block);
    self
  }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CalendarState {
  datetime: NaiveDate,
}

impl CalendarState {
  pub fn from_datetime(datetime: &DateTime<Utc>) -> Self {
    Self {
      datetime: datetime.date_naive(),
    }
  }

  pub fn today() -> Self {
    Self {
      datetime: chrono::Utc::now().date_naive(),
    }
  }

  pub fn to_utc_datetime(&self) -> DateTime<Utc> {
    let mut datetime = chrono::Utc::now();
    datetime = datetime
      .with_hour(0)
      .unwrap()
      .with_minute(0)
      .unwrap()
      .with_second(0)
      .unwrap();

    datetime = datetime
      .with_year(self.datetime.year())
      .unwrap()
      .with_month(self.datetime.month())
      .unwrap()
      .with_day(self.datetime.day())
      .unwrap();

    datetime
  }

  pub fn decrement(&mut self, days: i64) {
    self.datetime -= Duration::days(days);
  }

  pub fn increment(&mut self, days: i64) {
    self.datetime += Duration::days(days);
  }
}

impl<'a> StatefulWidget for Calendar<'a> {
  type State = CalendarState;

  fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
    if area.width < 22 || area.height < 8 {
      return;
    }

    buf.set_style(area, self.style);
    let calendar_area = match self.block.take() {
      Some(b) => {
        let inner_area = b.inner(area);
        b.render(area, buf);
        inner_area
      }
      None => area,
    };

    overheader(buf, calendar_area, state.datetime);
    header(buf, calendar_area, state.datetime);

    let today = chrono::Utc::now().date_naive();
    let days_in_month = total_days_in_month(state.datetime.year(), state.datetime.month()).unwrap();
    let mut current_date = state.datetime.with_day(1).unwrap();

    let mut printing_week = 1;
    let mut printing = true;

    while printing {
      let day_of_week = current_date.weekday();
      let day = current_date.day();
      let y = area.y + 2 + printing_week;
      let x = area.x + 2 + day_x_offset(day, day_of_week);

      let mut style = Style::default();

      if current_date == today {
        style = style.add_modifier(Modifier::UNDERLINED);
      };
      if day == state.datetime.day() {
        style = style.bg(Color::Red);
      };

      buf.set_string(x, y, format!("{day}"), style);

      if day == days_in_month {
        printing = false;
      } else {
        if day_of_week == Weekday::Sat {
          printing_week += 1;
        }
        current_date += Duration::days(1);
      }
    }
  }
}

impl<'a> Widget for Calendar<'a> {
  fn render(self, area: Rect, buf: &mut Buffer) {
    let mut state = CalendarState::today();
    StatefulWidget::render(self, area, buf, &mut state);
  }
}

fn centered_line_offset(text_width: u16, area_width: u16, area_offset: u16) -> u16 {
  (area_width / 2).saturating_sub(text_width / 2) + area_offset
}

fn overheader(buf: &mut Buffer, area: Rect, datetime: NaiveDate) {
  let text = datetime.format("%-d %B %C%y").to_string();
  let x = centered_line_offset(text.len().try_into().unwrap(), area.width, area.x);
  buf.set_string(x, area.y, text, Style::default());
}

fn header(buf: &mut Buffer, area: Rect, _datetime: NaiveDate) {
  let header_style = Style::default().add_modifier(Modifier::UNDERLINED);
  let text = "Su Mo Tu We Th Fr Sa";
  let x = centered_line_offset(text.len().try_into().unwrap(), area.width, area.x);
  buf.set_string(x, area.y + 1, text, header_style);
}

fn day_x_offset(day: u32, weekday: Weekday) -> u16 {
  let weekday_offset = match weekday {
    Weekday::Sun => 0,
    Weekday::Mon => 3,
    Weekday::Tue => 6,
    Weekday::Wed => 9,
    Weekday::Thu => 12,
    Weekday::Fri => 15,
    Weekday::Sat => 18,
  };

  if day < 10 {
    weekday_offset + 1
  } else {
    weekday_offset
  }
}

fn total_days_in_month(year: i32, month: u32) -> Option<u32> {
  let days = NaiveDate::from_ymd_opt(
    match month {
      12 => year + 1,
      _ => year,
    },
    match month {
      12 => 1,
      _ => month + 1,
    },
    1,
  )
  .unwrap()
  .signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
  .num_days();

  u32::try_from(days).ok()
}