Implement Slack flavored markdown reporting for report tasks
This commit is contained in:
parent
2c25aabc6a
commit
11fbaa582e
8 changed files with 715 additions and 2 deletions
16
src/main.rs
16
src/main.rs
|
|
@ -1,3 +1,15 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
mod things;
|
||||
mod reporter;
|
||||
|
||||
use reporter::{MarkdownReporter, Reporter};
|
||||
|
||||
use things::task::Task;
|
||||
use anyhow::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let today = Task::today()?;
|
||||
let reported: Vec<Task> = today.into_iter().filter(|task| task.has_tag("Report")).collect();
|
||||
println!("{}", MarkdownReporter.report(reported));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
183
src/reporter.rs
Normal file
183
src/reporter.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
use crate::things::task::Task;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProjectTree {
|
||||
id: String,
|
||||
title: String,
|
||||
tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AreaTree {
|
||||
id: String,
|
||||
title: String,
|
||||
projects: Vec<ProjectTree>,
|
||||
hanging_tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ThingsTree {
|
||||
areas: Vec<AreaTree>,
|
||||
hanging_tasks: Vec<Task>,
|
||||
}
|
||||
|
||||
impl ProjectTree {
|
||||
/// Add the task to this project if it belongs here, otherwise pass it back out.
|
||||
pub fn try_take_task(&mut self, mut task: Task) -> Option<Task> {
|
||||
if let Some(proj) = task.project {
|
||||
if proj.id == self.id {
|
||||
task.project = Some(proj);
|
||||
self.tasks.push(task);
|
||||
return None;
|
||||
}
|
||||
task.project = Some(proj);
|
||||
return Some(task);
|
||||
}
|
||||
return Some(task);
|
||||
}
|
||||
}
|
||||
|
||||
impl AreaTree {
|
||||
pub fn add_new_project_and_task(&mut self, mut task: Task) {
|
||||
if let Some(proj) = task.project {
|
||||
let id = proj.id.clone();
|
||||
let title = proj.title.clone();
|
||||
task.project = Some(proj);
|
||||
self.projects.push(ProjectTree {
|
||||
id,
|
||||
title,
|
||||
tasks: vec![task],
|
||||
});
|
||||
} else {
|
||||
self.hanging_tasks.push(task);
|
||||
}
|
||||
}
|
||||
pub fn try_take_task(&mut self, mut task: Task) -> Option<Task> {
|
||||
if let Some(area) = task.area {
|
||||
if area.id == self.id {
|
||||
task.area = Some(area);
|
||||
let mut maybe_task = Some(task);
|
||||
for proj in self.projects.iter_mut() {
|
||||
if let Some(t) = maybe_task {
|
||||
maybe_task = proj.try_take_task(t);
|
||||
}
|
||||
}
|
||||
// Here, the task belongs in this area but there was no project for it.
|
||||
maybe_task.map(|t| self.add_new_project_and_task(t));
|
||||
return None;
|
||||
}
|
||||
task.area = Some(area);
|
||||
return Some(task);
|
||||
}
|
||||
return Some(task);
|
||||
}
|
||||
}
|
||||
|
||||
impl ThingsTree {
|
||||
pub fn new() -> ThingsTree {
|
||||
ThingsTree { areas: vec![], hanging_tasks: vec![] }
|
||||
}
|
||||
|
||||
pub fn add_new_area_and_task(&mut self, mut task: Task) {
|
||||
if let Some(area) = task.area {
|
||||
let id = area.id.clone();
|
||||
let title = area.title.clone();
|
||||
task.area = Some(area);
|
||||
let mut area_tree = AreaTree {
|
||||
id,
|
||||
title,
|
||||
projects: vec![],
|
||||
hanging_tasks: vec![],
|
||||
};
|
||||
let took = area_tree.try_take_task(task);
|
||||
if took.is_some() {
|
||||
panic!("Area should have matched the task because it was created with the task");
|
||||
}
|
||||
self.areas.push(area_tree);
|
||||
} else {
|
||||
self.hanging_tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_take_task(&mut self, task: Task) {
|
||||
let mut maybe_task = Some(task);
|
||||
for area in self.areas.iter_mut() {
|
||||
if let Some(t) = maybe_task {
|
||||
maybe_task = area.try_take_task(t);
|
||||
}
|
||||
}
|
||||
maybe_task.map(|t| self.add_new_area_and_task(t));
|
||||
}
|
||||
|
||||
pub fn from_tasks(tasks: Vec<Task>) -> ThingsTree {
|
||||
let mut tree = ThingsTree::new();
|
||||
for task in tasks.into_iter() {
|
||||
tree.try_take_task(task);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Reporter {
|
||||
fn report_task(&mut self, task: &Task, depth: usize) -> String;
|
||||
fn report_project(&mut self, project: &ProjectTree, depth: usize) -> String;
|
||||
fn report_single_area(&mut self, area: &AreaTree) -> String;
|
||||
fn report_multiple_areas(&mut self, areas: &Vec<AreaTree>) -> String;
|
||||
fn report(&mut self, tasks: Vec<Task>) -> String {
|
||||
let tree = ThingsTree::from_tasks(tasks);
|
||||
let untracked_tasks = tree.hanging_tasks
|
||||
.iter()
|
||||
.map(|t| self.report_task(t, 0))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
let area_tasks: String = match tree.areas.len() {
|
||||
0 => "".to_string(),
|
||||
1 => self.report_single_area(&tree.areas[0]),
|
||||
_ => self.report_multiple_areas(&tree.areas),
|
||||
};
|
||||
|
||||
let separator = if area_tasks == "" || untracked_tasks == "" {
|
||||
""
|
||||
} else {
|
||||
"\n\n"
|
||||
};
|
||||
|
||||
format!("{}{}{}", area_tasks, separator, untracked_tasks)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MarkdownReporter;
|
||||
|
||||
impl Reporter for MarkdownReporter {
|
||||
fn report_task(&mut self, task: &Task, depth: usize) -> String {
|
||||
format!("{}- {}", String::from(" ").repeat(depth), task.title)
|
||||
}
|
||||
fn report_project(&mut self, project: &ProjectTree, depth: usize) -> String {
|
||||
let tasks = project.tasks.iter().map(|t| self.report_task(t, depth + 4)).collect::<Vec<String>>().join("\n");
|
||||
format!("{}{}\n{}", String::from(" ").repeat(depth), project.title, tasks)
|
||||
}
|
||||
fn report_single_area(&mut self, area: &AreaTree) -> String {
|
||||
let project_tasks = area.projects.iter().map(|p| self.report_project(p, 0)).collect::<Vec<String>>().join("\n");
|
||||
let untracked_tasks = area.hanging_tasks.iter().map(|t| self.report_task(t, 0)).collect::<Vec<String>>().join("\n");
|
||||
let separator = if project_tasks == "" || untracked_tasks == "" {
|
||||
""
|
||||
} else {
|
||||
"\n\n"
|
||||
};
|
||||
format!("{}{}{}", project_tasks, separator, untracked_tasks)
|
||||
}
|
||||
fn report_multiple_areas(&mut self, areas: &Vec<AreaTree>) -> String {
|
||||
match areas.len() {
|
||||
0 => "".to_string(),
|
||||
1 => self.report_single_area(&areas[0]),
|
||||
_ => {
|
||||
areas.iter().map(|area| {
|
||||
let single = self.report_single_area(area);
|
||||
format!("*{}*\n{}", area.title, single)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/things/logbook.js
Normal file
36
src/things/logbook.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
var things = Application("Things");
|
||||
var logbook = things.lists.byName("Logbook").toDos();
|
||||
var objs = [];
|
||||
|
||||
var from = new Date();
|
||||
from.setHours(0);
|
||||
from.setMinutes(0);
|
||||
from.setSeconds(0);
|
||||
var to = new Date();
|
||||
to.setTime(from.getTime());
|
||||
to.setHours(23);
|
||||
to.setMinutes(59);
|
||||
to.setSeconds(59);
|
||||
|
||||
logbook.filter(task => {
|
||||
return task.completionDate() >= from && task.completionDate() < to;
|
||||
}).forEach(todo => {
|
||||
var proj = todo.project();
|
||||
var tags = [];
|
||||
if (proj) {
|
||||
tags.push(...proj.tagNames().split(', '));
|
||||
}
|
||||
var area = todo.area() || proj && proj.area();
|
||||
objs.push({
|
||||
id: todo.id(),
|
||||
title: todo.name(),
|
||||
notes: todo.notes() || null,
|
||||
status: todo.status(),
|
||||
completion_date: todo.completionDate(),
|
||||
project: proj && { id: proj.id(), title: proj.name() },
|
||||
area: area && { id: area.id(), title: area.name() },
|
||||
tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t),
|
||||
});
|
||||
});
|
||||
|
||||
return JSON.stringify(objs, undefined, 2);
|
||||
1
src/things/mod.rs
Normal file
1
src/things/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod task;
|
||||
64
src/things/task.rs
Normal file
64
src/things/task.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use std::str::from_utf8;
|
||||
use serde::Deserialize;
|
||||
use anyhow::Result;
|
||||
use osascript;
|
||||
use serde_json;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub enum Status {
|
||||
#[serde(rename = "completed")]
|
||||
Completed,
|
||||
#[serde(rename = "incomplete")]
|
||||
Incomplete,
|
||||
#[serde(rename = "open")]
|
||||
Open,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Project {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Area {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Task {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub notes: Option<String>,
|
||||
pub status: Status,
|
||||
pub tags: Vec<String>,
|
||||
pub completion_date: Option<DateTime<Utc>>,
|
||||
pub project: Option<Project>,
|
||||
pub area: Option<Area>,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
/// A Helper for loading Tasks from json returned by an osascript
|
||||
fn from_script(script_bytes: &[u8]) -> Result<Vec<Task>> {
|
||||
let script = osascript::JavaScript::new(from_utf8(script_bytes)?);
|
||||
let raw_json: String = script.execute()?;
|
||||
let tasks: Vec<Task> = serde_json::from_str(&raw_json)?;
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
/// Get all tasks in the today list from Things
|
||||
pub fn today() -> Result<Vec<Task>> {
|
||||
Task::from_script(include_bytes!("today.js"))
|
||||
}
|
||||
|
||||
/// Get all tasks in the logbook list from Things
|
||||
pub fn logbook() -> Result<Vec<Task>> {
|
||||
Task::from_script(include_bytes!("logbook.js"))
|
||||
}
|
||||
|
||||
pub fn has_tag(&self, tag: &str) -> bool {
|
||||
self.tags.contains(&String::from(tag))
|
||||
}
|
||||
}
|
||||
24
src/things/today.js
Normal file
24
src/things/today.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
var things = Application("Things");
|
||||
var today = things.lists.byName("Today").toDos();
|
||||
var objs = [];
|
||||
|
||||
today.forEach(todo => {
|
||||
var proj = todo.project();
|
||||
var tags = [];
|
||||
if (proj) {
|
||||
tags.push(...proj.tagNames().split(', '));
|
||||
}
|
||||
var area = todo.area() || proj && proj.area();
|
||||
objs.push({
|
||||
id: todo.id(),
|
||||
title: todo.name(),
|
||||
notes: todo.notes() || null,
|
||||
status: todo.status(),
|
||||
completion_date: todo.completionDate(),
|
||||
project: proj && { id: proj.id(), title: proj.name() },
|
||||
area: area && { id: area.id(), title: area.name() },
|
||||
tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t),
|
||||
});
|
||||
});
|
||||
|
||||
return JSON.stringify(objs, undefined, 2);
|
||||
Reference in a new issue