use std::fmt;
use clap::ValueEnum;
use colored::Colorize as _;
use serde::{Deserialize, Serialize};
pub mod error {
use std::fmt;
pub enum Error {
AlreadyChecked {
id: u32,
},
AlreadyUnchecked {
id: u32,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::AlreadyChecked { id } => write!(f, "Task {id} was already checked"),
Self::AlreadyUnchecked { id } => write!(f, "Task {id} was already unchecked",),
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum Priority {
High,
Med,
Low,
None,
}
impl<T: AsRef<str>> From<T> for Priority {
#[inline]
fn from(s: T) -> Self {
match s.as_ref().to_lowercase().trim() {
"high" => Self::High,
"low" => Self::Low,
"none" => Self::None,
_ => Self::Med,
}
}
}
impl Priority {
#[inline]
pub const fn to_str(&self) -> &str {
match *self {
Self::High => "high",
Self::Med => "med",
Self::Low => "low",
Self::None => "none",
}
}
}
impl fmt::Display for Priority {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::High => write!(f, "high"),
Self::Med => write!(f, "med"),
Self::Low => write!(f, "low"),
Self::None => write!(f, "none"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub struct Task {
pub id: u32,
pub content: String,
pub priority: Priority,
pub checked: bool,
}
impl fmt::Display for Task {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = format!("{}. {}", self.id, self.content);
let colored = match self.priority {
Priority::High => msg.red(),
Priority::Med => msg.yellow(),
Priority::Low => msg.blue(),
Priority::None => msg.white(),
};
let bold = colored.bold();
let styled = if self.checked { bold.strikethrough() } else { bold };
write!(f, "{styled}")
}
}
impl Default for Task {
#[inline]
fn default() -> Self {
Self {
id: 0,
content: String::new(),
priority: Priority::Med,
checked: false,
}
}
}
impl Task {
#[inline]
pub const fn new(id: u32, content: String, priority: Priority, checked: bool) -> Self {
Self { id, content, priority, checked }
}
#[inline]
pub fn from<T: AsRef<str>>(line: T) -> Self {
let (id, content, priority, checked) = Self::split(line.as_ref());
Self { id, content, priority, checked }
}
#[inline]
pub fn split<T: AsRef<str>>(line: T) -> (u32, String, Priority, bool) {
let list: Vec<&str> = line.as_ref().split(',').map(str::trim).collect();
let id = list
.first()
.unwrap()
.parse()
.expect("ID field parsed incorrectly; must be a natural number");
let content = list.get(1).unwrap().trim().to_owned();
let priority = list.get(2).map_or(Priority::Med, Priority::from);
let checked = list
.get(3)
.is_some_and(|&s| matches!(s.trim(), "true" | "1"));
(id, content, priority, checked)
}
#[inline]
pub fn as_line(&self) -> String {
format!("{},{},{},{}", self.id, self.content, self.priority, self.checked)
}
#[inline]
pub const fn check(&mut self) -> Result<&Self, error::Error> {
if self.checked {
Err(error::Error::AlreadyChecked { id: self.id })
} else {
self.checked = true;
Ok(self)
}
}
#[inline]
pub const fn uncheck(&mut self) -> Result<&Self, error::Error> {
if self.checked {
self.checked = false;
Ok(self)
} else {
Err(error::Error::AlreadyUnchecked { id: self.id })
}
}
}