Parse tag blocks out of project and task note sections
This commit is contained in:
parent
469320a77f
commit
0144071046
6 changed files with 102 additions and 30 deletions
21
src/main.rs
21
src/main.rs
|
|
@ -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(())
|
||||||
|
|
|
||||||
104
src/reporter.rs
104
src/reporter.rs
|
|
@ -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(¬es, &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(¬es, &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>>()
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Reference in a new issue