#![forbid(unsafe_code)]
use std::fs::{self, File};
use std::io::Write;
use std::ops::Sub;
use std::str::FromStr;
use chrono::{Local, NaiveDate};
use ratatui::{layout::Alignment, text::Text};
use crate::{
TfError,
config::Config,
files::{FileStatus, FileWithTasks},
modes::Mode,
priority::Priority,
};
#[derive(Clone, Debug)]
pub struct TaskCount {
pub date: NaiveDate,
pub incomplete: Option<usize>,
pub complete: Option<usize>,
pub p1_incomp: Option<usize>,
pub p1_comp: Option<usize>,
pub p2_incomp: Option<usize>,
pub p2_comp: Option<usize>,
pub p3_incomp: Option<usize>,
pub p3_comp: Option<usize>,
pub p4_incomp: Option<usize>,
pub p4_comp: Option<usize>,
pub p5_incomp: Option<usize>,
pub p5_comp: Option<usize>,
pub recurring_incomp: Option<usize>,
pub non_recurring_incomp: Option<usize>,
pub recurring_comp: Option<usize>,
pub non_recurring_comp: Option<usize>,
}
impl TaskCount {
pub fn new(line: &str) -> Self {
let elements: Vec<String> = line.split(',').map(str::to_string).collect();
Self {
date: NaiveDate::from_str(&elements[0]).unwrap(),
incomplete: elements.get(1).and_then(|x| x.parse().ok()),
complete: elements.get(2).and_then(|x| x.parse().ok()),
p1_incomp: elements.get(3).and_then(|x| x.parse().ok()),
p1_comp: elements.get(4).and_then(|x| x.parse().ok()),
p2_incomp: elements.get(5).and_then(|x| x.parse().ok()),
p2_comp: elements.get(6).and_then(|x| x.parse().ok()),
p3_incomp: elements.get(7).and_then(|x| x.parse().ok()),
p3_comp: elements.get(8).and_then(|x| x.parse().ok()),
p4_incomp: elements.get(9).and_then(|x| x.parse().ok()),
p4_comp: elements.get(10).and_then(|x| x.parse().ok()),
p5_incomp: elements.get(11).and_then(|x| x.parse().ok()),
p5_comp: elements.get(12).and_then(|x| x.parse().ok()),
recurring_incomp: elements.get(13).and_then(|x| x.parse().ok()),
recurring_comp: elements.get(14).and_then(|x| x.parse().ok()),
non_recurring_incomp: elements.get(15).and_then(|x| x.parse().ok()),
non_recurring_comp: elements.get(16).and_then(|x| x.parse().ok()),
}
}
pub fn as_strings(&self) -> Vec<String> {
vec![
self.date.to_string(),
self.incomplete.map_or("".to_string(), |x| x.to_string()),
self.complete.map_or("".to_string(), |x| x.to_string()),
self.p1_incomp.map_or("".to_string(), |x| x.to_string()),
self.p1_comp.map_or("".to_string(), |x| x.to_string()),
self.p2_incomp.map_or("".to_string(), |x| x.to_string()),
self.p2_comp.map_or("".to_string(), |x| x.to_string()),
self.p3_incomp.map_or("".to_string(), |x| x.to_string()),
self.p3_comp.map_or("".to_string(), |x| x.to_string()),
self.recurring_incomp
.map_or("".to_string(), |x| x.to_string()),
self.recurring_comp
.map_or("".to_string(), |x| x.to_string()),
self.non_recurring_incomp
.map_or("".to_string(), |x| x.to_string()),
self.non_recurring_comp
.map_or("".to_string(), |x| x.to_string()),
]
}
pub fn labels() -> Vec<Text<'static>> {
vec![
Text::from("date").alignment(Alignment::Left),
Text::from("incomplete").alignment(Alignment::Right),
Text::from("complete").alignment(Alignment::Right),
Text::from("p1[ ]").alignment(Alignment::Right),
Text::from("p1[x]").alignment(Alignment::Right),
Text::from("p2[ ]").alignment(Alignment::Right),
Text::from("p2[x]").alignment(Alignment::Right),
Text::from("p3[ ]").alignment(Alignment::Right),
Text::from("p3[x]").alignment(Alignment::Right),
Text::from("r[ ]").alignment(Alignment::Right),
Text::from("r[x]").alignment(Alignment::Right),
Text::from("nr[ ]").alignment(Alignment::Right),
Text::from("nr[x]").alignment(Alignment::Right),
]
}
pub fn extract(files: &[FileWithTasks], mode: &Mode) -> TaskCount {
let date = Local::now().date_naive();
let mut task_count = TaskCount {
date,
incomplete: Some(0),
complete: Some(0),
p1_incomp: Some(0),
p1_comp: Some(0),
p2_incomp: Some(0),
p2_comp: Some(0),
p3_incomp: Some(0),
p3_comp: Some(0),
p4_incomp: Some(0),
p4_comp: Some(0),
p5_incomp: Some(0),
p5_comp: Some(0),
recurring_incomp: Some(0),
recurring_comp: Some(0),
non_recurring_incomp: Some(0),
non_recurring_comp: Some(0),
};
for file_with_task in files {
for task_set in &file_with_task.task_sets {
for task in &task_set.tasks {
if task.completed {
if let Some(v) = task_count.complete {
task_count.complete = Some(v + 1)
} else {
task_count.complete = Some(1)
}
if task.recurring {
if let Some(v) = task_count.recurring_comp {
task_count.recurring_comp = Some(v + 1)
} else {
task_count.recurring_comp = Some(1)
}
} else if let Some(v) = task_count.non_recurring_comp {
task_count.non_recurring_comp = Some(v + 1)
} else {
task_count.non_recurring_comp = Some(1)
}
if let Some(v) = task.priority {
match v {
Priority::One => {
if let Some(v) = task_count.p1_comp {
task_count.p1_comp = Some(v + 1)
} else {
task_count.p1_comp = Some(1)
}
}
Priority::Two => {
if let Some(v) = task_count.p2_comp {
task_count.p2_comp = Some(v + 1)
} else {
task_count.p2_comp = Some(1)
}
}
Priority::Three => {
if let Some(v) = task_count.p3_comp {
task_count.p3_comp = Some(v + 1)
} else {
task_count.p3_comp = Some(1)
}
}
Priority::Four => {
if let Some(v) = task_count.p4_comp {
task_count.p4_comp = Some(v + 1)
} else {
task_count.p4_comp = Some(1)
}
}
Priority::Five => {
if let Some(v) = task_count.p5_comp {
task_count.p5_comp = Some(v + 1)
} else {
task_count.p5_comp = Some(1)
}
}
Priority::NoPriority => (),
}
}
} else if !task.completed {
if mode == &Mode::Log && file_with_task.status != FileStatus::Active {
continue;
}
if task.recurring {
if let Some(v) = task_count.recurring_incomp {
task_count.recurring_incomp = Some(v + 1)
} else {
task_count.recurring_incomp = Some(1)
}
} else if let Some(v) = task_count.non_recurring_incomp {
task_count.non_recurring_incomp = Some(v + 1)
} else {
task_count.non_recurring_incomp = Some(1)
}
if let Some(v) = task_count.incomplete {
task_count.incomplete = Some(v + 1)
} else {
task_count.incomplete = Some(1)
}
if let Some(v) = task.priority {
match v {
Priority::One => {
if let Some(v) = task_count.p1_incomp {
task_count.p1_incomp = Some(v + 1)
} else {
task_count.p1_incomp = Some(1)
}
}
Priority::Two => {
if let Some(v) = task_count.p2_incomp {
task_count.p2_incomp = Some(v + 1)
} else {
task_count.p2_incomp = Some(1)
}
}
Priority::Three => {
if let Some(v) = task_count.p3_incomp {
task_count.p3_incomp = Some(v + 1)
} else {
task_count.p3_incomp = Some(1)
}
}
Priority::Four => {
if let Some(v) = task_count.p4_incomp {
task_count.p4_incomp = Some(v + 1)
} else {
task_count.p4_incomp = Some(1)
}
}
Priority::Five => {
if let Some(v) = task_count.p5_incomp {
task_count.p5_incomp = Some(v + 1)
} else {
task_count.p5_incomp = Some(1)
}
}
Priority::NoPriority => (),
}
}
}
}
}
}
task_count
}
pub fn log(config: &Config, task_count: TaskCount) -> Result<(), TfError> {
let date = Local::now().date_naive();
let contents = fs::read_to_string(&config.num_tasks_log)?;
if !contents.contains(&format!("{date}")) {
let mut f = File::options()
.append(true)
.open(config.num_tasks_log.clone())?;
writeln!(
&mut f,
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
task_count.date,
task_count.incomplete.unwrap_or_default(),
task_count.complete.unwrap_or_default(),
task_count.p1_incomp.unwrap_or_default(),
task_count.p1_comp.unwrap_or_default(),
task_count.p2_incomp.unwrap_or_default(),
task_count.p2_comp.unwrap_or_default(),
task_count.p3_incomp.unwrap_or_default(),
task_count.p3_comp.unwrap_or_default(),
task_count.p4_incomp.unwrap_or_default(),
task_count.p4_comp.unwrap_or_default(),
task_count.p5_incomp.unwrap_or_default(),
task_count.p5_comp.unwrap_or_default(),
task_count.recurring_incomp.unwrap_or_default(),
task_count.recurring_comp.unwrap_or_default(),
task_count.non_recurring_incomp.unwrap_or_default(),
task_count.non_recurring_comp.unwrap_or_default(),
)?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TaskCountDifference {
pub days_ago: i64,
pub incomplete: String,
pub complete: String,
pub p1_incomp: String,
pub p1_comp: String,
pub p2_incomp: String,
pub p2_comp: String,
pub p3_incomp: String,
pub p3_comp: String,
pub recurring_incomp: String,
pub non_recurring_incomp: String,
pub recurring_comp: String,
pub non_recurring_comp: String,
}
fn diff_count(lhs: Option<usize>, rhs: Option<usize>) -> String {
if let Some(v) = lhs
&& let Some(w) = rhs
{
format!("{}", (v as isize - w as isize))
} else {
"".to_string()
}
}
impl Sub for TaskCount {
type Output = TaskCountDifference;
fn sub(self, rhs: Self) -> Self::Output {
TaskCountDifference {
days_ago: (self.date - rhs.date).num_days(),
incomplete: diff_count(self.incomplete, rhs.incomplete),
complete: diff_count(self.complete, rhs.complete),
p1_incomp: diff_count(self.p1_incomp, rhs.p1_incomp),
p1_comp: diff_count(self.p1_comp, rhs.p1_comp),
p2_incomp: diff_count(self.p2_incomp, rhs.p2_incomp),
p2_comp: diff_count(self.p2_comp, rhs.p2_comp),
p3_incomp: diff_count(self.p3_incomp, rhs.p3_incomp),
p3_comp: diff_count(self.p3_comp, rhs.p3_comp),
recurring_incomp: diff_count(self.recurring_incomp, rhs.recurring_incomp),
non_recurring_incomp: diff_count(self.non_recurring_incomp, rhs.non_recurring_incomp),
recurring_comp: diff_count(self.recurring_comp, rhs.recurring_comp),
non_recurring_comp: diff_count(self.non_recurring_comp, rhs.non_recurring_comp),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::make_test_config;
#[test]
fn task_count_is_correct() {
let config = make_test_config();
let files = FileWithTasks::collect(&config).unwrap();
let task_count = TaskCount::extract(&files, &Mode::Log);
assert_eq!(task_count.complete, Some(18));
assert_eq!(task_count.p1_comp, Some(1));
assert_eq!(task_count.p2_comp, Some(1));
assert_eq!(task_count.p3_comp, Some(1));
assert_eq!(task_count.p4_comp, Some(0));
assert_eq!(task_count.p5_comp, Some(0));
assert_eq!(task_count.incomplete, Some(18));
assert_eq!(task_count.p1_incomp, Some(0));
assert_eq!(task_count.p2_incomp, Some(2));
assert_eq!(task_count.p3_incomp, Some(1));
assert_eq!(task_count.p4_incomp, Some(0));
assert_eq!(task_count.p5_incomp, Some(0));
assert_eq!(task_count.recurring_incomp, Some(2)); assert_eq!(task_count.non_recurring_incomp, Some(16)); }
#[test]
fn task_count_diff_is_correct() {
let count1 = TaskCount {
date: NaiveDate::from_str("2026-02-05").unwrap(),
incomplete: Some(25),
complete: Some(25),
p1_incomp: Some(5),
p1_comp: Some(5),
p2_incomp: Some(5),
p2_comp: Some(5),
p3_incomp: Some(5),
p3_comp: Some(5),
p4_incomp: Some(5),
p4_comp: Some(5),
p5_incomp: Some(5),
p5_comp: Some(5),
recurring_incomp: Some(10),
non_recurring_incomp: Some(15),
recurring_comp: Some(15),
non_recurring_comp: Some(10),
};
let count2 = TaskCount {
date: NaiveDate::from_str("2026-02-10").unwrap(),
incomplete: Some(20),
complete: Some(30),
p1_incomp: Some(3),
p1_comp: Some(7),
p2_incomp: Some(2),
p2_comp: Some(8),
p3_incomp: Some(4),
p3_comp: Some(6),
p4_incomp: Some(5),
p4_comp: Some(5),
p5_incomp: Some(5),
p5_comp: Some(5),
recurring_incomp: Some(7),
non_recurring_incomp: Some(13),
recurring_comp: Some(18),
non_recurring_comp: Some(12),
};
let count_diff = count2 - count1;
let expected_diff = TaskCountDifference {
days_ago: 5,
incomplete: String::from("-5"),
complete: String::from("5"),
p1_incomp: String::from("-2"),
p1_comp: String::from("2"),
p2_incomp: String::from("-3"),
p2_comp: String::from("3"),
p3_incomp: String::from("-1"),
p3_comp: String::from("1"),
recurring_incomp: String::from("-3"),
non_recurring_incomp: String::from("-2"),
recurring_comp: String::from("3"),
non_recurring_comp: String::from("2"),
};
assert_eq!(count_diff, expected_diff)
}
}