use std::collections::HashMap;
use std::path::Path;
use color_eyre::owo_colors::OwoColorize;
use time::formatting::Formattable;
use time::{Date, OffsetDateTime};
use leafslug_effects::files::ToFileName;
use super::task::State;
use super::tasklist::TaskDescription;
use super::Result;
use super::{
task::{StateKind, Task},
Error,
};
pub fn new_task(
task_dir: &Path,
t: &Task,
repo_root: &str,
time_format_descriptor: &(impl Formattable + ?Sized),
) -> Result<()> {
let file_name = t.to_file_name(time_format_descriptor)?;
let file_path = task_dir.join(&file_name);
let fp = file_path.to_string_lossy().into_owned();
let content = serde_json::to_string_pretty(&t)
.map_err(|e| Error::FileCouldNotSerializeEntryIntoJson(e, file_name.clone()))?
.into_bytes();
leafslug_effects::create_dir(task_dir.to_path_buf(), true)?;
leafslug_effects::write_to_file(content, &file_path, true, false)?;
leafslug_effects::git_add(repo_root, [fp])?;
leafslug_effects::git_commit(
repo_root,
&format!("feat(journal): add new journal entry {file_name}"),
)?;
leafslug_effects::git_pull(repo_root)?;
leafslug_effects::git_push(repo_root)?;
Ok(())
}
pub fn mark_task_as(
task_dir: &Path,
tasks_list: impl Iterator<Item = TaskDescription>,
state: &State,
repo_root: &str,
task_identifier: &(impl IntoIterator<Item = i64> + Clone),
) -> Result<()> {
tasks_list
.filter(|x| -> bool {
task_identifier
.clone()
.into_iter()
.any(|iden| x.task.id.to_string().contains(&iden.to_string()))
})
.filter(|x| {
let Some(last_state) = x.task.state_log.last() else {
return false;
};
last_state.ne(state)
})
.try_for_each(|mut t| {
t.task.state_log.push(state.clone());
let file_path = task_dir.join(&t.file_name);
let file_path_str = file_path.to_string_lossy().into_owned();
let new_content = serde_json::to_string_pretty(&t.task)
.map_err(|e| Error::FileCouldNotSerializeEntryIntoJson(e, t.file_name))?
.into_bytes();
leafslug_effects::write_to_file(new_content, &file_path, false, true)?;
leafslug_effects::git_add(repo_root, [file_path_str])?;
leafslug_effects::git_commit(
repo_root,
&format!(
"feat: updated task {} to the new state {}",
t.task.id, state.kind,
),
)?;
Ok::<_, Error>(())
})?;
leafslug_effects::git_pull(repo_root)?;
leafslug_effects::git_push(repo_root)?;
Ok(())
}
pub fn todays_task(
all_tasks: impl Iterator<Item = TaskDescription>,
current_date: Date,
of_project: &Option<String>,
current_time: OffsetDateTime,
time_format_descriptor: &(impl Formattable + ?Sized),
) -> Result<()> {
let mut strting = vec![];
let mut ddln = vec![];
let mut ovrdu = vec![];
all_tasks.into_iter().for_each(|t| {
let Some(last) = t.task.state_log.last() else {
return;
};
if !matches!(last.kind, StateKind::ToDo | StateKind::Abandoned(_)) {
return;
};
if let Some(deadlined) = t.task.end {
if current_date == deadlined {
match of_project {
Some(proj) => {
if t.task.projects.contains(proj) {
ddln.push(t.clone())
};
}
None => ddln.push(t.clone()),
}
};
if current_date > deadlined {
match of_project {
Some(proj) => {
if t.task.projects.contains(proj) {
ovrdu.push(t.clone())
};
}
None => ovrdu.push(t.clone()),
}
}
};
if let Some(bst) = t.task.start {
if current_date == bst {
match of_project {
Some(proj) => {
if t.task.projects.contains(proj) {
strting.push(t)
};
}
None => strting.push(t),
}
};
};
});
println!();
if !strting.is_empty() {
println!(
"\n{:61}",
"Starting from today:".bold().black().on_bright_blue()
);
strting
.iter()
.map(|x| {
try_print_colorful_with_current_duration(
&x.task,
current_time,
time_format_descriptor,
)
})
.for_each(println_ok_or_eprintln);
}
if !ddln.is_empty() {
println!("\n{:61}", "Deadline at today:".bold().black().on_red());
ddln.iter()
.map(|x| {
try_print_colorful_with_current_duration(
&x.task,
current_time,
time_format_descriptor,
)
})
.for_each(println_ok_or_eprintln);
}
if !ovrdu.is_empty() {
println!("\n{:61}", "overdue at today:".bold().black().on_red());
ovrdu
.iter()
.map(|x| {
try_print_colorful_with_current_duration(
&x.task,
current_time,
time_format_descriptor,
)
})
.for_each(println_ok_or_eprintln);
}
Ok(())
}
fn println_ok_or_eprintln(x: Result<String>) {
match x {
Ok(f) => println!("{f}"),
Err(e) => println!("{e}"),
}
}
pub fn tasks_by_state<F>(
all_tasks: impl Iterator<Item = TaskDescription>,
task_state_finder: F,
of_project: &Option<String>,
current_time: OffsetDateTime,
time_format_descriptor: &(impl Formattable + ?Sized),
) -> Result<()>
where
F: Fn(&StateKind) -> bool,
{
let chosen_tasks: Vec<TaskDescription> = all_tasks
.filter(|task_description| {
let Some(last) = task_description.task.state_log.last() else {
return false;
};
if !task_state_finder(&last.kind) {
return false;
};
let Some(proj) = &of_project else {
return true;
};
task_description.task.projects.contains(proj)
})
.collect();
if !chosen_tasks.is_empty() {
println!(
"{:61}",
"tasks with that criteria:".bold().black().on_bright_blue()
);
chosen_tasks
.iter()
.map(|x| {
try_print_colorful_with_current_duration(
&x.task,
current_time,
time_format_descriptor,
)
})
.for_each(println_ok_or_eprintln);
}
Ok(())
}
fn try_print_colorful_with_current_duration(
x: &Task,
current_time: OffsetDateTime,
time_format_descriptor: &(impl Formattable + ?Sized),
) -> std::result::Result<String, Error> {
Ok(format!(
"\n{}",
x.print_colorful_with_current_duration(current_time, time_format_descriptor)?
))
}
pub fn bulk_task_editor(
all_tasks: impl Iterator<Item = TaskDescription>,
now: OffsetDateTime,
repo_root: &str,
task_dir: &Path,
) -> Result<()> {
let tasks: Vec<_> = all_tasks
.filter(|t| {
let Some(last_state) = t.task.state_log.last() else {
return false;
};
!matches!(last_state.kind, StateKind::Done | StateKind::Abandoned(_))
})
.collect();
let before: HashMap<_, _> = tasks
.iter()
.map(|x| Ok((x.task.id, TaskAction::try_from(&x.task.clone())?)))
.collect::<Result<_>>()?;
let current = form_file_action_json(&before)?;
let requested = dialoguer::Editor::new()
.extension(".json")
.edit(¤t)
.map_err(Error::FailedInRunningEditor)?
.ok_or(Error::EmptyEditorContent)?;
let changed: HashMap<i64, TaskAction> =
remove_unchanged(before, parse_action_from_json(&requested)?).collect();
tasks
.iter()
.filter_map(|td| Some((td.clone(), changed.get(&td.task.id)?)))
.map(|(mut td, ta)| {
td.task.title = ta.title.clone();
td.task.state_log.push(State::new(now, ta.state.clone()));
td
})
.try_for_each(|t| -> Result<()> {
let fp = task_dir.join(t.file_name.clone());
let new_file_content = serde_json::to_string_pretty(&t.task)
.map_err(|e| Error::FileCouldNotSerializeEntryIntoJson(e, t.file_name.clone()))?
.into_bytes();
leafslug_effects::write_to_file(new_file_content, &fp, false, true)?;
leafslug_effects::git_add(repo_root, [fp.to_string_lossy().into_owned()])?;
leafslug_effects::git_commit(
repo_root,
&format!("feat(tasks): updated task {}", t.task.id),
)?;
Ok(())
})?;
leafslug_effects::git_pull(repo_root)?;
leafslug_effects::git_push(repo_root)?;
Ok(())
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TaskAction {
pub state: StateKind,
pub title: String,
}
impl TryFrom<&Task> for TaskAction {
type Error = Error;
fn try_from(value: &Task) -> std::result::Result<Self, Self::Error> {
Ok(Self {
state: value
.state_log
.last()
.map(|s| &s.kind)
.cloned()
.ok_or(Error::EveryTaskShouldHaveAtLeastOneState)?,
title: value.title.clone(),
})
}
}
pub fn transfer_list(tasks: &[Task]) -> Result<HashMap<i64, TaskAction>> {
tasks
.iter()
.map(|t| Ok((t.id, TaskAction::try_from(t)?)))
.collect()
}
pub fn form_file_action_json<A: serde::Serialize, B: serde::Serialize>(
tasks: &HashMap<A, B>,
) -> Result<String> {
serde_json::to_string_pretty(tasks).map_err(Error::FileCouldNotSerializeTaskActionsIntoJson)
}
pub fn parse_action_from_json(raw: &str) -> Result<HashMap<i64, TaskAction>> {
serde_json::from_str(raw)
.map_err(|e| Error::FileCouldNotDeserializeEntryFromJson(e, raw.to_owned()))
}
pub fn remove_unchanged(
before: HashMap<i64, TaskAction>,
after: HashMap<i64, TaskAction>,
) -> impl Iterator<Item = (i64, TaskAction)> {
before.into_iter().filter(move |(id, be)| {
let Some(af) = after.get(id) else {
return false;
};
be.ne(af)
})
}