leafslug_tsk 0.4.0

A personal Task Management System that is not inteded to be used by others... yet.
Documentation
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(&current)
        .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)
    })
}