Update the day reporter to support more arbitrary reporting from things lists and between arbitrary dates

This commit is contained in:
Campbell Alden 2024-12-24 12:28:07 +09:00
parent 9d62d4d555
commit fb711c2810
6 changed files with 68 additions and 34 deletions

2
Cargo.lock generated
View file

@ -173,7 +173,7 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]] [[package]]
name = "day-reporter" name = "day-reporter"
version = "1.4.1" version = "2.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "day-reporter" name = "day-reporter"
version = "1.4.1" version = "2.0.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -18,24 +18,21 @@ enum ListType {
} }
impl ListType { impl ListType {
fn format_tasks(&self, tasks: Vec<Task>, tags: &Vec<String>, sanitize_names: bool) -> String { fn format_tasks(&self, tasks: Vec<Task>, tags: &Vec<String>, sanitize_names: bool, resolution: Resolution) -> String {
match self { match self {
ListType::Today => { ListType::Today => {
let task_report = MarkdownReporter.report(tasks, &ReportOptions { MarkdownReporter.report(tasks, &ReportOptions {
resolution: Resolution::FullTask, resolution,
tags: tags.to_vec(), tags: tags.to_vec(),
sanitize_names, sanitize_names,
}); })
// TODO: Use a cli flag to determine if the emoji should be included.
format!("{}\n\n{}", emoji::pick(3).join(" "), task_report)
}, },
ListType::Logbook => { ListType::Logbook => {
let task_report = MarkdownReporter.report(tasks, &ReportOptions { MarkdownReporter.report(tasks, &ReportOptions {
resolution: Resolution::FullTask, resolution,
tags: tags.to_vec(), tags: tags.to_vec(),
sanitize_names, sanitize_names,
}); })
format!("Stopping now\n\n{}", task_report)
}, },
} }
} }
@ -63,20 +60,47 @@ struct CliArgs {
/// Select the type of report to generate /// Select the type of report to generate
#[arg(short, long, default_value_t = ListType::default())] #[arg(short, long, default_value_t = ListType::default())]
#[clap(value_enum)] #[clap(value_enum)]
report: ListType, list: ListType,
/// By default, any @<name> style tags will be sanitized in the output to avoid @-mentions in /// By default, any @<name> style tags will be sanitized in the output to avoid @-mentions in
/// Slack. This is done by replacing vowel characters with unicode lookalikes. If this /// Slack. This is done by replacing vowel characters with unicode lookalikes. If this
/// flag is set then the names will be passed through unsanitized. /// flag is set then the names will be passed through unsanitized.
#[arg(long, default_value_t = false)] #[arg(long, default_value_t = false)]
no_sanitize: bool, no_sanitize: bool,
/// An ISO date string for when to filter tasks from.
/// Defaults to midnight this morning if unset. Not used for the today list
#[arg(long)]
from: Option<String>,
/// An ISO date string for when to filter tasks until.
/// Defaults to 1 second before midnight tonight if unset. Not used for the today list
#[arg(long)]
to: Option<String>,
/// An optional message to include at the beginning of the report. If omitted, 3 random emojis
/// will be included instead
#[arg(short, long)]
message: Option<String>,
/// Choose a resolution for the report. This will determine how much detail is included in the
/// output. Default is "FullTask"
#[arg(short, long, default_value_t = Resolution::default())]
#[clap(value_enum)]
resolution: Resolution,
} }
fn main() -> Result<()> { fn main() -> Result<()> {
let args = CliArgs::parse(); let args = CliArgs::parse();
let tasks = match args.report { let now = chrono::Local::now();
ListType::Today => Task::today(), let from = args.from.unwrap_or_else(|| now.date().and_hms(0, 0, 0).to_rfc3339());
ListType::Logbook => Task::logbook(), let to = args.to.unwrap_or_else(|| (now + chrono::Duration::days(1)).date().and_hms(0, 0, 0).to_rfc3339());
let message = args.message.unwrap_or_else(|| emoji::pick(3).join(" "));
let tasks = match args.list {
ListType::Today => Task::today(&from, &to),
ListType::Logbook => Task::logbook(&from, &to),
}?; }?;
let mut reported: Vec<Task> = tasks.into_iter().filter(|task| { let mut reported: Vec<Task> = tasks.into_iter().filter(|task| {
// Filter down to tasks with all selected tags and without any of the omitted tags // Filter down to tasks with all selected tags and without any of the omitted tags
@ -85,8 +109,8 @@ fn main() -> Result<()> {
reported.sort_by(|a, b| { reported.sort_by(|a, b| {
a.completion_date.cmp(&b.completion_date) a.completion_date.cmp(&b.completion_date)
}); });
let report = args.report.format_tasks(reported, &args.tags, !args.no_sanitize); let report = args.list.format_tasks(reported, &args.tags, !args.no_sanitize, args.resolution);
println!("{report}"); println!("{message}\n\n{report}");
Ok(()) Ok(())
} }

View file

@ -1,3 +1,5 @@
use clap::ValueEnum;
use crate::things::task::{Task, Status}; use crate::things::task::{Task, Status};
use crate::names::sanitize_names; use crate::names::sanitize_names;
@ -126,11 +128,18 @@ impl ThingsTree {
} }
} }
#[derive(ValueEnum, Copy, Clone, Eq, PartialEq)]
pub enum Resolution { pub enum Resolution {
FullTask, FullTask,
Project, Project,
} }
impl Default for Resolution {
fn default() -> Resolution {
Resolution::FullTask
}
}
pub struct ReportOptions { pub struct ReportOptions {
pub resolution: Resolution, pub resolution: Resolution,
pub tags: Vec<String>, pub tags: Vec<String>,

View file

@ -2,15 +2,8 @@ var things = Application("Things");
var logbook = things.lists.byName("Logbook").toDos(); var logbook = things.lists.byName("Logbook").toDos();
var objs = []; var objs = [];
var from = new Date(); var from = new Date($params.from);
from.setHours(0); var to = new Date($params.to);
from.setMinutes(0);
from.setSeconds(0);
var to = new Date();
to.setTime(from.getTime());
to.setHours(23);
to.setMinutes(59);
to.setSeconds(59);
for (const todo of logbook) { for (const todo of logbook) {
if(todo.completionDate() >= from && todo.completionDate() < to) { if(todo.completionDate() >= from && todo.completionDate() < to) {

View file

@ -1,5 +1,5 @@
use std::str::from_utf8; use std::str::from_utf8;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use anyhow::Result; use anyhow::Result;
use osascript; use osascript;
use serde_json; use serde_json;
@ -44,23 +44,31 @@ pub struct Task {
pub area: Option<Area>, pub area: Option<Area>,
} }
#[derive(Serialize, Debug)]
pub struct TaskParams {
// ISO string for the start date
from: String,
// ISO string for the end date
to: String,
}
impl Task { impl Task {
/// A Helper for loading Tasks from json returned by an osascript /// A Helper for loading Tasks from json returned by an osascript
fn from_script(script_bytes: &[u8]) -> Result<Vec<Task>> { fn from_script(script_bytes: &[u8], params: TaskParams) -> Result<Vec<Task>> {
let script = osascript::JavaScript::new(from_utf8(script_bytes)?); let script = osascript::JavaScript::new(from_utf8(script_bytes)?);
let raw_json: String = script.execute()?; let raw_json: String = script.execute_with_params(params)?;
let tasks: Vec<Task> = serde_json::from_str(&raw_json)?; let tasks: Vec<Task> = serde_json::from_str(&raw_json)?;
Ok(tasks) Ok(tasks)
} }
/// Get all tasks in the today list from Things /// Get all tasks in the today list from Things
pub fn today() -> Result<Vec<Task>> { pub fn today(from: &String, to: &String) -> Result<Vec<Task>> {
Task::from_script(include_bytes!("today.js")) Task::from_script(include_bytes!("today.js"), TaskParams { from: from.to_string(), to: to.to_string() })
} }
/// Get all tasks in the logbook list from Things /// Get all tasks in the logbook list from Things
pub fn logbook() -> Result<Vec<Task>> { pub fn logbook(from: &String, to: &String) -> Result<Vec<Task>> {
Task::from_script(include_bytes!("logbook.js")) Task::from_script(include_bytes!("logbook.js"), TaskParams { from: from.to_string(), to: to.to_string() })
} }
pub fn has_tag(&self, tag: &str) -> bool { pub fn has_tag(&self, tag: &str) -> bool {