use core::fmt;
use std::fmt::Display;
use crate::api::serialize::{todoist_due_date, todoist_rfc3339};
use crate::api::tree::Treeable;
use chrono::{DateTime, FixedOffset, Utc};
use owo_colors::{OwoColorize, Stream};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use super::{ProjectID, SectionID};
pub type TaskID = String;
pub type UserID = String;
fn default_task_url() -> Url {
"https://todoist.com/".parse().unwrap()
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
pub struct Task {
pub id: TaskID,
pub project_id: ProjectID,
pub section_id: Option<SectionID>,
pub content: String,
pub description: String,
pub checked: bool,
pub labels: Vec<String>,
pub parent_id: Option<TaskID>,
pub child_order: isize,
pub priority: Priority,
pub due: Option<DueDate>,
#[serde(default = "default_task_url")]
pub url: Url,
pub note_count: usize,
pub user_id: UserID,
pub added_by_uid: Option<UserID>,
pub responsible_uid: Option<UserID>,
pub assigned_by_uid: Option<UserID>,
#[serde(serialize_with = "todoist_rfc3339")]
pub added_at: DateTime<Utc>,
}
impl Treeable for Task {
type ID = TaskID;
fn id(&self) -> TaskID {
self.id.clone()
}
fn parent_id(&self) -> Option<TaskID> {
self.parent_id.clone()
}
fn reset_parent(&mut self) {
self.parent_id = None;
}
}
impl Ord for Task {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (
self.due
.as_ref()
.map(|d| d.exact.as_ref().map(|e| e.datetime))
.unwrap_or_default(),
other
.due
.as_ref()
.map(|d| d.exact.as_ref().map(|e| e.datetime))
.unwrap_or_default(),
) {
(Some(left), Some(right)) => match left.cmp(&right) {
std::cmp::Ordering::Equal => {}
ord => return ord,
},
(Some(_left), None) => return std::cmp::Ordering::Less,
(None, Some(_right)) => return std::cmp::Ordering::Greater,
(None, None) => {}
}
match self.priority.cmp(&other.priority).reverse() {
core::cmp::Ordering::Equal => {}
ord => return ord,
}
match self.child_order.cmp(&other.child_order) {
core::cmp::Ordering::Equal => {}
ord => return ord,
}
self.id.cmp(&other.id)
}
}
impl PartialOrd for Task {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(
Default, Debug, Copy, Clone, Serialize_repr, Deserialize_repr, PartialEq, Eq, PartialOrd, Ord,
)]
#[repr(u8)]
pub enum Priority {
#[default]
Normal = 1,
High = 2,
VeryHigh = 3,
Urgent = 4,
}
impl Display for Priority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Priority::Normal => write!(f, "p4"),
Priority::High => write!(
f,
"{}",
"p3".if_supports_color(Stream::Stdout, |text| text.blue())
),
Priority::VeryHigh => write!(
f,
"{}",
"p2".if_supports_color(Stream::Stdout, |text| text.yellow())
),
Priority::Urgent => write!(
f,
"{}",
"p1".if_supports_color(Stream::Stdout, |text| text.red())
),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct ExactTime {
pub datetime: DateTime<FixedOffset>,
pub timezone: String,
}
impl Display for ExactTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Ok(tz) = self.timezone.parse::<chrono_tz::Tz>() {
write!(f, "{}", self.datetime.with_timezone(&tz))
} else {
write!(f, "{}", self.datetime)
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct DueDate {
#[serde(rename = "string")]
pub string: String,
#[serde(deserialize_with = "todoist_due_date")]
pub date: chrono::NaiveDate,
pub is_recurring: bool,
#[serde(flatten)]
pub exact: Option<ExactTime>,
}
pub struct DueDateFormatter<'a>(pub &'a DueDate, pub &'a DateTime<Utc>);
impl Display for DueDateFormatter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_recurring {
write!(
f,
"{}",
"[REPEAT] ".if_supports_color(Stream::Stdout, |_| "🔁 ")
)?;
}
if let Some(exact) = &self.0.exact {
if exact.datetime >= *self.1 {
write!(
f,
"{}",
exact.if_supports_color(Stream::Stdout, |text| text.bright_green())
)
} else {
write!(
f,
"{}",
exact.if_supports_color(Stream::Stdout, |text| text.bright_red())
)
}
} else if self.0.date >= self.1.date_naive() {
write!(
f,
"{}",
self.0
.string
.if_supports_color(Stream::Stdout, |text| text.bright_green())
)
} else {
write!(
f,
"{}",
self.0
.string
.if_supports_color(Stream::Stdout, |text| text.bright_red())
)
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum TaskDue {
#[serde(rename = "due_string")]
String(String),
#[serde(rename = "due_date")]
Date(String),
#[serde(rename = "due_datetime", serialize_with = "todoist_rfc3339")]
DateTime(DateTime<Utc>),
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct CreateTask {
pub content: String,
pub description: Option<String>,
pub project_id: Option<ProjectID>,
pub section_id: Option<SectionID>,
pub parent_id: Option<TaskID>,
pub order: Option<isize>,
pub labels: Vec<String>,
pub priority: Option<Priority>,
#[serde(flatten)]
pub due: Option<TaskDue>,
pub due_lang: Option<String>,
pub assignee: Option<UserID>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct UpdateTask {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<Priority>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub due: Option<TaskDue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub due_lang: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assignee: Option<UserID>,
}
#[cfg(test)]
impl Task {
pub fn new(id: &str, content: &str) -> Task {
Task {
id: id.to_string(),
project_id: "".to_string(),
section_id: None,
content: content.to_string(),
description: String::new(),
checked: false,
labels: Vec::new(),
parent_id: None,
child_order: 0,
priority: Priority::default(),
due: None,
url: "http://localhost".to_string().parse().unwrap(),
note_count: 0,
user_id: "0".to_string(),
added_by_uid: None,
responsible_uid: None,
assigned_by_uid: None,
added_at: Utc::now(),
}
}
}