Parse tag blocks out of project and task note sections

This commit is contained in:
Campbell Alden 2023-07-24 11:02:14 +09:00
parent 469320a77f
commit 0144071046
6 changed files with 102 additions and 30 deletions

View file

@ -2,7 +2,7 @@ mod things;
mod reporter; mod reporter;
mod emoji; mod emoji;
use reporter::{MarkdownReporter, Reporter, Resolution}; use reporter::{MarkdownReporter, Reporter, Resolution, ReportOptions};
use things::task::{Task, Status}; use things::task::{Task, Status};
use anyhow::Result; use anyhow::Result;
@ -20,14 +20,20 @@ enum Modes {
} }
impl Modes { impl Modes {
fn format_tasks(&self, tasks: Vec<Task>) -> String { fn format_tasks(&self, tasks: Vec<Task>, tags: Vec<String>) -> String {
match self { match self {
Modes::Morning => { Modes::Morning => {
let task_report = MarkdownReporter.report(tasks, &Resolution::FullTask); let task_report = MarkdownReporter.report(tasks, &ReportOptions {
resolution: Resolution::FullTask,
tags,
});
format!("{}\n\n{}", emoji::pick(3).join(" "), task_report) format!("{}\n\n{}", emoji::pick(3).join(" "), task_report)
}, },
Modes::Signoff => { Modes::Signoff => {
let task_report = MarkdownReporter.report(tasks, &Resolution::FullTask); let task_report = MarkdownReporter.report(tasks, &ReportOptions {
resolution: Resolution::FullTask,
tags,
});
format!("Stopping now\n\n{}", task_report) format!("Stopping now\n\n{}", task_report)
}, },
Modes::Cycle => { Modes::Cycle => {
@ -37,7 +43,10 @@ impl Modes {
} }
return false; return false;
}).collect::<Vec<Task>>(); }).collect::<Vec<Task>>();
let task_report = MarkdownReporter.report(further_filtered, &Resolution::Project); let task_report = MarkdownReporter.report(further_filtered, &ReportOptions {
resolution: Resolution::Project,
tags,
});
format!("*Cycle Report*\n\n{}", task_report) format!("*Cycle Report*\n\n{}", task_report)
}, },
} }
@ -73,7 +82,7 @@ fn main() -> Result<()> {
let reported: Vec<Task> = tasks.into_iter().filter(|task| { let reported: Vec<Task> = tasks.into_iter().filter(|task| {
args.tags.iter().all(|tag| task.has_tag(tag)) args.tags.iter().all(|tag| task.has_tag(tag))
}).collect(); }).collect();
let report = args.mode.format_tasks(reported); let report = args.mode.format_tasks(reported, args.tags);
println!("{report}"); println!("{report}");
Ok(()) Ok(())

View file

@ -1,9 +1,41 @@
use crate::things::task::Task; use crate::things::task::Task;
/// Given a notes field and a list of possible tags for sections, return the content of triple tick
/// blocks containing those tags
///
/// # Examples
/// ```
/// extract_tagged_notes(
/// "\`\`\`report
/// Something
/// \`\`\`",
/// vec![String::from("report")],
/// ); // -> "Something"
/// ```
///
fn extract_tagged_notes(notes: &str, tags: &Vec<String>) -> Vec<String> {
notes
.split("```")
.into_iter()
.map(|section| -> (&str, Option<&String>) {
(section, tags.iter().find(|t| section.starts_with(*t)))
})
.filter(|(_section, tag)| { tag.is_some() })
.map(|(section, tag)| {
let start_tag = tag.unwrap();
section.strip_prefix(start_tag)
.map(|s| s.trim())
.expect("Failed to strip start tag prefix").to_string()
})
.collect()
}
#[derive(Debug)] #[derive(Debug)]
pub struct ProjectTree { pub struct ProjectTree {
id: String, id: String,
title: String, title: String,
notes: Option<String>,
tasks: Vec<Task>, tasks: Vec<Task>,
} }
@ -39,6 +71,7 @@ impl AreaTree {
self.projects.push(ProjectTree { self.projects.push(ProjectTree {
id: project.id.clone(), id: project.id.clone(),
title: project.title.clone(), title: project.title.clone(),
notes: project.notes.clone(),
tasks: vec![task], tasks: vec![task],
}); });
} }
@ -69,6 +102,7 @@ impl ThingsTree {
self.hanging_projects.push(ProjectTree { self.hanging_projects.push(ProjectTree {
id: project.id.clone(), id: project.id.clone(),
title: project.title.clone(), title: project.title.clone(),
notes: project.notes.clone(),
tasks: vec![task], tasks: vec![task],
}); });
} }
@ -93,22 +127,27 @@ pub enum Resolution {
Project, Project,
} }
pub struct ReportOptions {
pub resolution: Resolution,
pub tags: Vec<String>,
}
pub trait Reporter { pub trait Reporter {
fn report_task(&mut self, task: &Task, depth: usize) -> String; fn report_task(&mut self, task: &Task, depth: usize, options: &ReportOptions) -> String;
fn report_project(&mut self, project: &ProjectTree, depth: usize, resolution: &Resolution) -> String; fn report_project(&mut self, project: &ProjectTree, depth: usize, options: &ReportOptions) -> String;
fn report_single_area(&mut self, area: &AreaTree, resolution: &Resolution) -> String; fn report_single_area(&mut self, area: &AreaTree, options: &ReportOptions) -> String;
fn report_multiple_areas(&mut self, areas: &Vec<AreaTree>, resolution: &Resolution) -> String; fn report_multiple_areas(&mut self, areas: &Vec<AreaTree>, options: &ReportOptions) -> String;
fn report(&mut self, tasks: Vec<Task>, resolution: &Resolution) -> String { fn report(&mut self, tasks: Vec<Task>, options: &ReportOptions) -> String {
let tree = ThingsTree::from_tasks(tasks); let tree = ThingsTree::from_tasks(tasks);
let untracked_tasks = tree.hanging_tasks let untracked_tasks = tree.hanging_tasks
.iter() .iter()
.map(|t| self.report_task(t, 0)) .map(|t| self.report_task(t, 0, options))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
let area_tasks: String = match tree.areas.len() { let area_tasks: String = match tree.areas.len() {
0 => "".to_string(), 0 => "".to_string(),
1 => self.report_single_area(&tree.areas[0], resolution), 1 => self.report_single_area(&tree.areas[0], options),
_ => self.report_multiple_areas(&tree.areas, resolution), _ => self.report_multiple_areas(&tree.areas, options),
}; };
let separator = if area_tasks == "" || untracked_tasks == "" { let separator = if area_tasks == "" || untracked_tasks == "" {
@ -124,44 +163,67 @@ pub trait Reporter {
pub struct MarkdownReporter; pub struct MarkdownReporter;
impl Reporter for MarkdownReporter { impl Reporter for MarkdownReporter {
fn report_task(&mut self, task: &Task, depth: usize) -> String { fn report_task(&mut self, task: &Task, depth: usize, options: &ReportOptions) -> String {
format!("{}- {}", String::from(" ").repeat(depth), task.title) let relevant_notes = task.notes.clone()
.map(|notes| extract_tagged_notes(&notes, &options.tags))
.unwrap_or(vec![])
.iter()
.map(|l| format!("\n{}- {}", String::from(" ").repeat(depth + 4), l))
.collect::<Vec<String>>()
.join("");
format!("\n{}- {}{}", String::from(" ").repeat(depth), task.title, relevant_notes)
} }
fn report_project(&mut self, project: &ProjectTree, depth: usize, resolution: &Resolution) -> String { fn report_project(&mut self, project: &ProjectTree, depth: usize, options: &ReportOptions) -> String {
let resolution = &options.resolution;
let relevant_notes = project.notes.clone()
.map(|notes| extract_tagged_notes(&notes, &options.tags))
.unwrap_or(vec![])
.iter()
.map(|l| format!("\n{}- {}", String::from(" ").repeat(depth + 4), l))
.collect::<Vec<String>>()
.join("");
match resolution { match resolution {
Resolution::FullTask => { Resolution::FullTask => {
let tasks = project.tasks.iter().map(|t| self.report_task(t, depth + 4)).collect::<Vec<String>>().join("\n"); let tasks = project.tasks
format!("{}{}\n{}", String::from(" ").repeat(depth), project.title, tasks) .iter()
.map(|t| self.report_task(t, depth + 4, options))
.collect::<Vec<String>>()
.join("");
format!("{}{}{}{}", String::from(" ").repeat(depth), relevant_notes, project.title, tasks)
}, },
Resolution::Project => { Resolution::Project => {
format!("{}- {}", String::from(" ").repeat(depth), project.title) format!("{}- {}{}", String::from(" ").repeat(depth), project.title, relevant_notes)
} }
} }
} }
fn report_single_area(&mut self, area: &AreaTree, resolution: &Resolution) -> String { fn report_single_area(&mut self, area: &AreaTree, options: &ReportOptions) -> String {
let project_reports = area.projects let project_reports = area.projects
.iter() .iter()
.map(|p| self.report_project(p, 0, resolution)) .map(|p| self.report_project(p, 0, options))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
let untracked_tasks = area.hanging_tasks.iter().map(|t| self.report_task(t, 0)).collect::<Vec<String>>().join("\n"); let untracked_tasks = area.hanging_tasks
.iter()
.map(|t| self.report_task(t, 0, options))
.collect::<Vec<String>>()
.join("");
let separator = if project_reports == "" || untracked_tasks == "" { let separator = if project_reports == "" || untracked_tasks == "" {
"" ""
} else { } else {
"\n\n" "\n\n"
}; };
match resolution { match options.resolution {
Resolution::FullTask => format!("{}{}{}", project_reports, separator, untracked_tasks), Resolution::FullTask => format!("{}{}{}", project_reports, separator, untracked_tasks),
Resolution::Project => format!("{}", project_reports) Resolution::Project => format!("{}", project_reports)
} }
} }
fn report_multiple_areas(&mut self, areas: &Vec<AreaTree>, resolution: &Resolution) -> String { fn report_multiple_areas(&mut self, areas: &Vec<AreaTree>, options: &ReportOptions) -> String {
match areas.len() { match areas.len() {
0 => "".to_string(), 0 => "".to_string(),
1 => self.report_single_area(&areas[0], resolution), 1 => self.report_single_area(&areas[0], options),
_ => { _ => {
areas.iter().map(|area| { areas.iter().map(|area| {
let single = self.report_single_area(area, resolution); let single = self.report_single_area(area, options);
format!("*{}*\n{}", area.title, single) format!("*{}*\n{}", area.title, single)
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()

View file

@ -27,7 +27,7 @@ logbook.filter(task => {
notes: todo.notes() || null, notes: todo.notes() || null,
status: todo.status(), status: todo.status(),
completion_date: todo.completionDate(), completion_date: todo.completionDate(),
project: proj && { id: proj.id(), title: proj.name(), status: proj.status() }, project: proj && { id: proj.id(), title: proj.name(), status: proj.status(), notes: proj.notes() },
area: area && { id: area.id(), title: area.name() }, area: area && { id: area.id(), title: area.name() },
tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t), tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t),
}); });

View file

@ -27,7 +27,7 @@ logbook.filter(task => {
notes: todo.notes() || null, notes: todo.notes() || null,
status: todo.status(), status: todo.status(),
completion_date: todo.completionDate(), completion_date: todo.completionDate(),
project: proj && { id: proj.id(), title: proj.name(), status: proj.status() }, project: proj && { id: proj.id(), title: proj.name(), status: proj.status(), notes: proj.notes() },
area: area && { id: area.id(), title: area.name() }, area: area && { id: area.id(), title: area.name() },
tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t), tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t),
}); });

View file

@ -19,6 +19,7 @@ pub enum Status {
pub struct Project { pub struct Project {
pub id: String, pub id: String,
pub title: String, pub title: String,
pub notes: Option<String>,
pub status: Status, pub status: Status,
} }

View file

@ -15,7 +15,7 @@ today.forEach(todo => {
notes: todo.notes() || null, notes: todo.notes() || null,
status: todo.status(), status: todo.status(),
completion_date: todo.completionDate(), completion_date: todo.completionDate(),
project: proj && { id: proj.id(), title: proj.name(), status: proj.status() }, project: proj && { id: proj.id(), title: proj.name(), status: proj.status(), notes: proj.notes() },
area: area && { id: area.id(), title: area.name() }, area: area && { id: area.id(), title: area.name() },
tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t), tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t),
}); });