use std::{fmt::Display, ops::Sub, path::PathBuf, str::FromStr};
use color_eyre::owo_colors::OwoColorize;
use time::{formatting::Formattable, Date, OffsetDateTime};
use leafslug_effects::files::ToFileName;
use super::Error;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Task {
pub id: i64,
#[serde(with = "time::serde::rfc3339")]
pub time_created: OffsetDateTime,
pub state_log: Vec<State>,
pub title: String,
pub description: Option<String>,
pub area: Option<Area>,
pub people: Vec<String>,
pub projects: Vec<String>,
pub start: Option<Date>,
pub end: Option<Date>,
}
impl Task {
pub fn print_colorful_with_current_duration(
&self,
current_time: OffsetDateTime,
time_format_description: &(impl Formattable + ?Sized),
) -> Result<String, Error> {
let mut all_buf = String::new();
all_buf.push_str(&format!(
"{}\t{} {}",
self.current_state()
.map(|s| s.kind.clone())
.ok_or(Error::EveryTaskShouldHaveAtLeastOneState)?
.black()
.on_magenta(),
"⍙".bright_black(),
self.id.bright_black().bold(),
));
all_buf.push('\n');
all_buf.push_str(&format!("⍜ {}", self.title.bold()));
all_buf.push('\n');
if let Some(description) = &self.description {
all_buf.push_str(&format!(
"{} {}",
"⍘".bright_black(),
&description.bright_black()
));
all_buf.push('\n');
}
all_buf.push_str(&{
let mut buf = String::new();
if let Some(content) = &self.area {
buf.push_str(&format!("{}{} ", "#".green(), content.green()));
};
self.projects.iter().for_each(|project| {
buf.push_str(&format!("{}{} ", "?".yellow(), project.yellow()));
});
self.people.iter().for_each(|person| {
buf.push_str(&format!("{}{} ", "@".blue().bold(), person.blue()));
});
buf
});
all_buf.push('\n');
all_buf.push_str(&{
let mut buf = String::new();
if let Some(at) = self.start {
buf.push_str(&format!(
"{}",
at.format(time_format_description)?.italic().bright_black()
));
};
buf.push_str(&format!("{}", " ⇰ ".bright_black().bold()));
if let Some(at) = self.end {
let until = at.sub(current_time.date());
buf.push_str(&format!(
"{}",
at.format(time_format_description)?.italic().bright_black()
));
buf.push_str(&format!(
"{}",
format!(" ◕ {}d{}h", until.whole_days(), until.whole_hours())
.bright_black()
.bold()
));
};
buf
});
Ok(all_buf)
}
#[must_use]
pub fn current_state(&self) -> Option<&State> {
self.state_log.last()
}
}
impl TryFrom<&PathBuf> for Task {
type Error = Error;
fn try_from(value: &PathBuf) -> Result<Self, Self::Error> {
let content = fs_extra::file::read_to_string(value).map_err(Error::FileCouldNotBeRead)?;
let task: Self = serde_json::from_str(&content)
.map_err(|e| Error::FileCouldNotDeserializeEntryFromJson(e, content))?;
Ok(task)
}
}
impl TryFrom<PathBuf> for Task {
type Error = Error;
fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
let content = fs_extra::file::read_to_string(value).map_err(Error::FileCouldNotBeRead)?;
let task: Self = serde_json::from_str(&content)
.map_err(|e| Error::FileCouldNotDeserializeEntryFromJson(e, content))?;
Ok(task)
}
}
impl ToFileName for Task {
type Error = Error;
fn to_file_name(
&self,
time_format_descriptor: &(impl Formattable + ?Sized),
) -> Result<String, Self::Error> {
Ok(self.time_created.format(time_format_descriptor)?)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct State {
#[serde(with = "time::serde::rfc3339")]
pub at: OffsetDateTime,
pub kind: StateKind,
}
impl State {
pub const fn new(at: OffsetDateTime, kind: StateKind) -> Self {
Self { at, kind }
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum StateKind {
Backlog,
Abandoned(Option<String>),
Done,
ToDo,
}
impl Display for StateKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Backlog => "⚏ BKLG",
Self::Abandoned(_) => "☓ ABND",
Self::Done => "☑ DONE",
Self::ToDo => "☐ TODO",
}
)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Area {
Work,
Home,
Personal,
}
impl Display for Area {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Work => "work",
Self::Home => "home",
Self::Personal => "personal",
}
)
}
}
impl FromStr for Area {
type Err = AreaParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"work" | "w" | "wo" | "wor" => Ok(Self::Work),
"home" | "h" | "ho" | "hom" => Ok(Self::Home),
"personal" | "p" | "pe" | "per" | "pers" | "perso" | "person" | "persona" => {
Ok(Self::Personal)
}
_ => Err(AreaParsingError::AreaIsNotAMatch),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, thiserror::Error)]
pub enum AreaParsingError {
#[error("area given is not matching any particular ones")]
AreaIsNotAMatch,
}
#[cfg(test)]
mod testing {
use super::*;
const fn is_normal<T: Sized + Send + Sync + Unpin>() {}
#[test]
const fn normal_types() {
is_normal::<Task>();
}
}