use crate::entities::prelude::{Ppl, SigDate};
use crate::notes::{self, Kind, NoteOps, Occurrence};
use crate::PplError;
use chrono::{Local, NaiveDate};
use clap::{Args, Subcommand};
use interim::{parse_date_string, Dialect};
use sea_orm::{DatabaseConnection, EntityTrait};
use std::io;
#[derive(Args, Debug)]
pub struct NoteArgs {
#[command(subcommand)]
pub action: Option<NoteAction>,
}
#[derive(Subcommand, Debug)]
pub enum NoteAction {
/// Notes and todos from today on
List {
/// Past ones too
#[arg(long, short)]
all: bool,
},
/// Add a note to the next occurrence of an event, e.g. `add thanksgiving bring pie`
Add {
/// Words from its name, like thanksgiving, "1:1" or "anji birthday" (quote several)
event: String,
#[arg(required = true, trailing_var_arg = true)]
text: Vec<String>,
/// A todo, which can be ticked off, rather than a note
#[arg(long, short)]
todo: bool,
/// The occurrence on this day instead of the next one: 2026-12-25, "next friday"
#[arg(long, short)]
date: Option<String>,
},
/// Tick a todo off (or back on)
Done { id: i32 },
/// Remove a note or todo
Rm { id: i32 },
}
fn invalid(msg: String) -> PplError {
io::Error::new(io::ErrorKind::InvalidInput, msg).into()
}
pub async fn do_notes(args: &NoteArgs, db: &DatabaseConnection) -> Result<(), PplError> {
match &args.action {
None => list(db, false).await,
Some(NoteAction::List { all }) => list(db, *all).await,
Some(NoteAction::Add {
event,
text,
todo,
date,
}) => add(db, event, &text.join(" "), *todo, date.as_deref()).await,
Some(NoteAction::Done { id }) => {
let n = NoteOps::toggle(db, *id).await?;
println!("{}", notes::line(&n));
Ok(())
}
Some(NoteAction::Rm { id }) => {
NoteOps::delete(db, *id).await?;
println!("removed note {}", id);
Ok(())
}
}
}
async fn add(
db: &DatabaseConnection,
query: &str,
text: &str,
todo: bool,
date: Option<&str>,
) -> Result<(), PplError> {
let today = Local::now().date_naive();
let (from, days) = match date {
Some(d) => (parse_day(d)?, 0),
None => (today, 400),
};
// Every word has to match, so "anji birthday" picks one of Anji's dates
let words = query.to_lowercase();
let words = words.split_whitespace().collect::<Vec<_>>();
let found = notes::upcoming(db, from, days)
.await?
.into_iter()
.filter(|o| {
let title = o.title.to_lowercase();
words.iter().all(|w| title.contains(w))
})
.collect::<Vec<_>>();
let Some(first) = found.first() else {
return Err(invalid(match date {
Some(d) => format!("nothing matching '{}' on {}", query, d),
None => format!("nothing matching '{}' in the next year", query),
}));
};
// Different events matched: say which, rather than guess
let mut events: Vec<&Occurrence> = vec![];
for o in &found {
if !events.iter().any(|e| e.target == o.target) {
events.push(o);
}
}
if events.len() > 1 {
let list = events
.iter()
.take(8)
.map(|o| format!(" {} {}", o.date.format("%a %Y-%m-%d"), o.title))
.collect::<Vec<_>>()
.join("\n");
return Err(invalid(format!(
"'{}' matches more than one event; be more specific:\n{}",
query, list
)));
}
let kind = if todo { Kind::Todo } else { Kind::Note };
let n = NoteOps::add(db, first, kind, text).await?;
println!(
"{} on {}, {} (#{})",
notes::line(&n),
first.title,
first.date.format("%a %Y-%m-%d"),
n.id
);
if date.is_none() && found.len() > 1 {
println!("(that's the next one; --date picks another)");
}
Ok(())
}
fn parse_day(s: &str) -> Result<NaiveDate, PplError> {
if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
return Ok(d);
}
parse_date_string(s, Local::now(), Dialect::Us)
.map(|d| d.date_naive())
.map_err(|_| invalid(format!("can't read '{}' as a date; try YYYY-MM-DD", s)))
}
async fn list(db: &DatabaseConnection, all: bool) -> Result<(), PplError> {
let today = Local::now().date_naive();
let mut all_notes = NoteOps::list(db).await?;
all_notes.retain(|n| all || n.date >= today);
all_notes.sort_by_key(|n| (n.date, n.id));
if all_notes.is_empty() {
println!("no notes; add one with `ppl notes add <event> <text>`");
return Ok(());
}
let ppl = Ppl::find().all(db).await?;
let dates = SigDate::find().all(db).await?;
let title = |n: &crate::entities::note::Model| match (&n.calendar, &n.event, n.sig_date_id) {
(Some(c), Some(e), _) => format!("{} ({})", e, c),
(_, _, Some(id)) => dates
.iter()
.find(|d| d.id == id)
.and_then(|d| {
let p = ppl.iter().find(|p| p.id == d.ppl_id)?;
Some(Occurrence::of_person(d, n.date, p).title)
})
.unwrap_or_else(|| "(a date that's gone)".to_string()),
_ => "?".to_string(),
};
let mut last: Option<(NaiveDate, String)> = None;
for n in &all_notes {
let heading = (n.date, title(n));
if last.as_ref() != Some(&heading) {
println!("{} {}", heading.0.format("%a %Y-%m-%d"), heading.1);
last = Some(heading);
}
println!(" #{:<4} {}", n.id, notes::line(n));
}
Ok(())
}
⭐
Log in to mark this repository.
5.6 KiB · text
History
d31eadb