⭐
Log in to mark this repository.
motd: --rich details, and notes and todos on upcoming dates
d31eadb · Gabriel Morell · 2026-10-19 17:26
✓
Rust CI and Docker Build / Create Release (push)
✓
Rust CI and Docker Build / Release (7z a, .exe, ppl-x86_64-windows.zip, windows-latest, windows-latest, 1.92) (push)
✓
Rust CI and Docker Build / Release (tar -czf, , ppl-aarch64-linux-gnu.tar.gz, ubuntu-24.04-arm, ubuntu-24.04-arm, 1.92) (push)
✓
Rust CI and Docker Build / Release (tar -czf, , ppl-aarch64-macos.tar.gz, macos-latest, macos-latest, 1.92) (push)
✓
Rust CI and Docker Build / Release (tar -czf, , ppl-x86_64-linux-gnu.tar.gz, ubuntu-latest, arc-runner-set, 1.92) (push)
✗
Rust CI and Docker Build / build (aarch64, ubuntu-24.04-arm) (push)
✓
Rust CI and Docker Build / build (x86_64, arc-runner-set) (push)
Message
{commit_body(@commit)}
Files changed
modified
README.md
+22
−0
@@ -50,6 +50,28 @@ with reminders off, and doesn't count as having shown anything, so the next plai
| 50 | 50 | reminds as usual. `--days 30` looks 30 days ahead for everyone and every calendar instead of their |
| 51 | 51 | windows, including people in no tier. |
| 52 | 52 | |
| 53 | +`ppl motd --rich` (with or without `--all`) prints what each holiday is under it, like the TUI's `i`: | |
| 54 | +the blurb and Wikipedia link for built-in holidays, and the location, description and link for iCal | |
| 55 | +events, wrapped to the terminal and cut at four lines. | |
| 56 | + | |
| 57 | +#### Notes and todos | |
| 58 | + | |
| 59 | +Any one occurrence of a date (this year's Thanksgiving, next Tuesday's 1:1, Anji's birthday) can carry | |
| 60 | +notes and todos, which `ppl motd --rich` prints under it. | |
| 61 | + | |
| 62 | +``` | |
| 63 | +ppl notes add thanksgiving bring pie # the next Thanksgiving | |
| 64 | +ppl notes add "anji birthday" --todo buy a card # every word has to match | |
| 65 | +ppl notes add "1:1" -d 2026-11-03 ask about budget | |
| 66 | +ppl notes # from today on (--all for past ones) | |
| 67 | +ppl notes done 3 # tick a todo (again to untick) | |
| 68 | +ppl notes rm 3 | |
| 69 | +``` | |
| 70 | + | |
| 71 | +In the TUI, `i` on any upcoming row (people's dates too) shows its notes: `n` adds a note, `t` a | |
| 72 | +todo, space ticks one, `Delete` twice removes one. Rows with notes get a 📝, or ☐ and the number of | |
| 73 | +open todos. | |
| 74 | + | |
| 53 | 75 | In the TUI the same keys work on the tiers tab (defaults) and on a selected tier while editing a person (overrides): |
| 54 | 76 | `m` cycles the mode, `+`/`-` change the window, `[`/`]` change the random %, `x` clears. |
| 55 | 77 |
added
src/do_notes.rs
+175
−0
@@ -0,0 +1,175 @@
| 1 | +use crate::entities::prelude::{Ppl, SigDate}; | |
| 2 | +use crate::notes::{self, Kind, NoteOps, Occurrence}; | |
| 3 | +use crate::PplError; | |
| 4 | +use chrono::{Local, NaiveDate}; | |
| 5 | +use clap::{Args, Subcommand}; | |
| 6 | +use interim::{parse_date_string, Dialect}; | |
| 7 | +use sea_orm::{DatabaseConnection, EntityTrait}; | |
| 8 | +use std::io; | |
| 9 | + | |
| 10 | +#[derive(Args, Debug)] | |
| 11 | +pub struct NoteArgs { | |
| 12 | + #[command(subcommand)] | |
| 13 | + pub action: Option<NoteAction>, | |
| 14 | +} | |
| 15 | + | |
| 16 | +#[derive(Subcommand, Debug)] | |
| 17 | +pub enum NoteAction { | |
| 18 | + /// Notes and todos from today on | |
| 19 | + List { | |
| 20 | + /// Past ones too | |
| 21 | + #[arg(long, short)] | |
| 22 | + all: bool, | |
| 23 | + }, | |
| 24 | + /// Add a note to the next occurrence of an event, e.g. `add thanksgiving bring pie` | |
| 25 | + Add { | |
| 26 | + /// Words from its name, like thanksgiving, "1:1" or "anji birthday" (quote several) | |
| 27 | + event: String, | |
| 28 | + #[arg(required = true, trailing_var_arg = true)] | |
| 29 | + text: Vec<String>, | |
| 30 | + /// A todo, which can be ticked off, rather than a note | |
| 31 | + #[arg(long, short)] | |
| 32 | + todo: bool, | |
| 33 | + /// The occurrence on this day instead of the next one: 2026-12-25, "next friday" | |
| 34 | + #[arg(long, short)] | |
| 35 | + date: Option<String>, | |
| 36 | + }, | |
| 37 | + /// Tick a todo off (or back on) | |
| 38 | + Done { id: i32 }, | |
| 39 | + /// Remove a note or todo | |
| 40 | + Rm { id: i32 }, | |
| 41 | +} | |
| 42 | + | |
| 43 | +fn invalid(msg: String) -> PplError { | |
| 44 | + io::Error::new(io::ErrorKind::InvalidInput, msg).into() | |
| 45 | +} | |
| 46 | + | |
| 47 | +pub async fn do_notes(args: &NoteArgs, db: &DatabaseConnection) -> Result<(), PplError> { | |
| 48 | + match &args.action { | |
| 49 | + None => list(db, false).await, | |
| 50 | + Some(NoteAction::List { all }) => list(db, *all).await, | |
| 51 | + Some(NoteAction::Add { | |
| 52 | + event, | |
| 53 | + text, | |
| 54 | + todo, | |
| 55 | + date, | |
| 56 | + }) => add(db, event, &text.join(" "), *todo, date.as_deref()).await, | |
| 57 | + Some(NoteAction::Done { id }) => { | |
| 58 | + let n = NoteOps::toggle(db, *id).await?; | |
| 59 | + println!("{}", notes::line(&n)); | |
| 60 | + Ok(()) | |
| 61 | + } | |
| 62 | + Some(NoteAction::Rm { id }) => { | |
| 63 | + NoteOps::delete(db, *id).await?; | |
| 64 | + println!("removed note {}", id); | |
| 65 | + Ok(()) | |
| 66 | + } | |
| 67 | + } | |
| 68 | +} | |
| 69 | + | |
| 70 | +async fn add( | |
| 71 | + db: &DatabaseConnection, | |
| 72 | + query: &str, | |
| 73 | + text: &str, | |
| 74 | + todo: bool, | |
| 75 | + date: Option<&str>, | |
| 76 | +) -> Result<(), PplError> { | |
| 77 | + let today = Local::now().date_naive(); | |
| 78 | + let (from, days) = match date { | |
| 79 | + Some(d) => (parse_day(d)?, 0), | |
| 80 | + None => (today, 400), | |
| 81 | + }; | |
| 82 | + // Every word has to match, so "anji birthday" picks one of Anji's dates | |
| 83 | + let words = query.to_lowercase(); | |
| 84 | + let words = words.split_whitespace().collect::<Vec<_>>(); | |
| 85 | + let found = notes::upcoming(db, from, days) | |
| 86 | + .await? | |
| 87 | + .into_iter() | |
| 88 | + .filter(|o| { | |
| 89 | + let title = o.title.to_lowercase(); | |
| 90 | + words.iter().all(|w| title.contains(w)) | |
| 91 | + }) | |
| 92 | + .collect::<Vec<_>>(); | |
| 93 | + let Some(first) = found.first() else { | |
| 94 | + return Err(invalid(match date { | |
| 95 | + Some(d) => format!("nothing matching '{}' on {}", query, d), | |
| 96 | + None => format!("nothing matching '{}' in the next year", query), | |
| 97 | + })); | |
| 98 | + }; | |
| 99 | + // Different events matched: say which, rather than guess | |
| 100 | + let mut events: Vec<&Occurrence> = vec![]; | |
| 101 | + for o in &found { | |
| 102 | + if !events.iter().any(|e| e.target == o.target) { | |
| 103 | + events.push(o); | |
| 104 | + } | |
| 105 | + } | |
| 106 | + if events.len() > 1 { | |
| 107 | + let list = events | |
| 108 | + .iter() | |
| 109 | + .take(8) | |
| 110 | + .map(|o| format!(" {} {}", o.date.format("%a %Y-%m-%d"), o.title)) | |
| 111 | + .collect::<Vec<_>>() | |
| 112 | + .join("\n"); | |
| 113 | + return Err(invalid(format!( | |
| 114 | + "'{}' matches more than one event; be more specific:\n{}", | |
| 115 | + query, list | |
| 116 | + ))); | |
| 117 | + } | |
| 118 | + let kind = if todo { Kind::Todo } else { Kind::Note }; | |
| 119 | + let n = NoteOps::add(db, first, kind, text).await?; | |
| 120 | + println!( | |
| 121 | + "{} on {}, {} (#{})", | |
| 122 | + notes::line(&n), | |
| 123 | + first.title, | |
| 124 | + first.date.format("%a %Y-%m-%d"), | |
| 125 | + n.id | |
| 126 | + ); | |
| 127 | + if date.is_none() && found.len() > 1 { | |
| 128 | + println!("(that's the next one; --date picks another)"); | |
| 129 | + } | |
| 130 | + Ok(()) | |
| 131 | +} | |
| 132 | + | |
| 133 | +fn parse_day(s: &str) -> Result<NaiveDate, PplError> { | |
| 134 | + if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") { | |
| 135 | + return Ok(d); | |
| 136 | + } | |
| 137 | + parse_date_string(s, Local::now(), Dialect::Us) | |
| 138 | + .map(|d| d.date_naive()) | |
| 139 | + .map_err(|_| invalid(format!("can't read '{}' as a date; try YYYY-MM-DD", s))) | |
| 140 | +} | |
| 141 | + | |
| 142 | +async fn list(db: &DatabaseConnection, all: bool) -> Result<(), PplError> { | |
| 143 | + let today = Local::now().date_naive(); | |
| 144 | + let mut all_notes = NoteOps::list(db).await?; | |
| 145 | + all_notes.retain(|n| all || n.date >= today); | |
| 146 | + all_notes.sort_by_key(|n| (n.date, n.id)); | |
| 147 | + if all_notes.is_empty() { | |
| 148 | + println!("no notes; add one with `ppl notes add <event> <text>`"); | |
| 149 | + return Ok(()); | |
| 150 | + } | |
| 151 | + let ppl = Ppl::find().all(db).await?; | |
| 152 | + let dates = SigDate::find().all(db).await?; | |
| 153 | + let title = |n: &crate::entities::note::Model| match (&n.calendar, &n.event, n.sig_date_id) { | |
| 154 | + (Some(c), Some(e), _) => format!("{} ({})", e, c), | |
| 155 | + (_, _, Some(id)) => dates | |
| 156 | + .iter() | |
| 157 | + .find(|d| d.id == id) | |
| 158 | + .and_then(|d| { | |
| 159 | + let p = ppl.iter().find(|p| p.id == d.ppl_id)?; | |
| 160 | + Some(Occurrence::of_person(d, n.date, p).title) | |
| 161 | + }) | |
| 162 | + .unwrap_or_else(|| "(a date that's gone)".to_string()), | |
| 163 | + _ => "?".to_string(), | |
| 164 | + }; | |
| 165 | + let mut last: Option<(NaiveDate, String)> = None; | |
| 166 | + for n in &all_notes { | |
| 167 | + let heading = (n.date, title(n)); | |
| 168 | + if last.as_ref() != Some(&heading) { | |
| 169 | + println!("{} {}", heading.0.format("%a %Y-%m-%d"), heading.1); | |
| 170 | + last = Some(heading); | |
| 171 | + } | |
| 172 | + println!(" #{:<4} {}", n.id, notes::line(n)); | |
| 173 | + } | |
| 174 | + Ok(()) | |
| 175 | +} |
modified
src/do_tui.rs
+79
−44
@@ -5,13 +5,13 @@ use crate::data::{
| 5 | 5 | use crate::do_init::lcolor; |
| 6 | 6 | use crate::entities::ppl::Model; |
| 7 | 7 | use crate::entities::{ |
| 8 | − calendar, contact, ppl, relation, sig_date, tier, tier_defaults, trait_defaults, traits, | |
| 8 | + calendar, contact, note, ppl, relation, sig_date, tier, tier_defaults, trait_defaults, traits, | |
| 9 | 9 | }; |
| 10 | 10 | use crate::holidays::{group_of, holidays_in, Holiday}; |
| 11 | +use crate::notes::{self, NoteOps, Occurrence}; | |
| 11 | 12 | use crate::tui_add::{render_person_menu, AddContext, AddForm, AddKind, FormAction}; |
| 12 | −use crate::tui_calendars::{ | |
| 13 | − holiday_color, open_url, render_holiday_info, CalendarMenu, MenuAction, | |
| 14 | −}; | |
| 13 | +use crate::tui_calendars::{holiday_color, open_url, CalendarMenu, MenuAction}; | |
| 14 | +use crate::tui_notes::{NotePopup, PopupAction}; | |
| 15 | 15 | use crate::PplError; |
| 16 | 16 | use chrono::{Datelike, Local, NaiveDate}; |
| 17 | 17 | use crossterm::event; |
@@ -77,9 +77,11 @@ struct Tui {
| 77 | 77 | /// Calendar rows, for their motd reminder settings |
| 78 | 78 | calendar_rows: Vec<calendar::Model>, |
| 79 | 79 | /// The holiday behind each row of the upcoming list (None for people's dates), for `i` |
| 80 | − upcoming_holidays: Vec<Option<Holiday>>, | |
| 80 | + upcoming_rows: Vec<(Occurrence, Option<Holiday>)>, | |
| 81 | 81 | /// The holiday whose blurb is showing |
| 82 | − holiday_info: Option<Holiday>, | |
| 82 | + note_popup: Option<NotePopup>, | |
| 83 | + /// Every note and todo | |
| 84 | + notes: Vec<note::Model>, | |
| 83 | 85 | /// The year the calendar tab shows; scrolling past either end moves it |
| 84 | 86 | cal_year: i32, |
| 85 | 87 | /// Holiday groups (by key) and "people" hidden on the calendar tab |
@@ -167,6 +169,7 @@ impl Tui {
| 167 | 169 | self.sigdate_list = SigDateOps::list(db).await?; |
| 168 | 170 | self.calendars_enabled = CalendarOps::enabled(db).await?; |
| 169 | 171 | self.calendar_rows = CalendarOps::rows(db).await?; |
| 172 | + self.notes = NoteOps::list(db).await?; | |
| 170 | 173 | self.holidays = holidays_in(self.cal_year, &self.calendars_enabled); |
| 171 | 174 | |
| 172 | 175 | Ok(()) |
@@ -212,8 +215,9 @@ impl Default for Tui {
| 212 | 215 | holidays: vec![], |
| 213 | 216 | calendar_menu: None, |
| 214 | 217 | calendar_rows: vec![], |
| 215 | − upcoming_holidays: vec![], | |
| 216 | − holiday_info: None, | |
| 218 | + upcoming_rows: vec![], | |
| 219 | + note_popup: None, | |
| 220 | + notes: vec![], | |
| 217 | 221 | cal_year: Local::now().year(), |
| 218 | 222 | cal_hidden: vec![], |
| 219 | 223 | } |
@@ -256,21 +260,38 @@ pub async fn run_tui(
| 256 | 260 | } |
| 257 | 261 | match ev { |
| 258 | 262 | Event::Key(key_event) |
| 259 | − if key_event.kind == KeyEventKind::Press && app.holiday_info.is_some() => | |
| 263 | + if key_event.kind == KeyEventKind::Press && app.note_popup.is_some() => | |
| 260 | 264 | { |
| 261 | − match (key_event.code, &app.holiday_info) { | |
| 262 | − (KeyCode::Char('o'), Some(h)) => { | |
| 263 | − let url = crate::holidays::about(h).url; | |
| 264 | − app.status = Some(match url.is_empty() { | |
| 265 | − true => "ℹ this event has no link".to_string(), | |
| 266 | − false => match open_url(&url) { | |
| 267 | − Ok(()) => format!("✓ opening {}", url), | |
| 268 | − Err(err) => format!("⚠ couldn't open a browser: {}", err), | |
| 269 | − }, | |
| 270 | − }); | |
| 265 | + let Some(mut popup) = app.note_popup.take() else { | |
| 266 | + continue; | |
| 267 | + }; | |
| 268 | + let occ = popup.occ.clone(); | |
| 269 | + let done = match popup.handle_key(&key_event, &app.notes) { | |
| 270 | + PopupAction::Continue => Ok(None), | |
| 271 | + PopupAction::Close => { | |
| 272 | + continue; | |
| 271 | 273 | } |
| 272 | − _ => app.holiday_info = None, | |
| 274 | + PopupAction::Status(msg) => Ok(Some(msg)), | |
| 275 | + PopupAction::Open(url) => Ok(Some(match open_url(&url) { | |
| 276 | + Ok(()) => format!("✓ opening {}", url), | |
| 277 | + Err(err) => format!("⚠ couldn't open a browser: {}", err), | |
| 278 | + })), | |
| 279 | + PopupAction::Add(kind, text) => NoteOps::add(&db, &occ, kind, &text) | |
| 280 | + .await | |
| 281 | + .map(|n| Some(format!("✓ added {}", notes::line(&n)))), | |
| 282 | + PopupAction::Toggle(id) => NoteOps::toggle(&db, id) | |
| 283 | + .await | |
| 284 | + .map(|n| Some(format!("✓ {}", notes::line(&n)))), | |
| 285 | + PopupAction::Delete(id) => NoteOps::delete(&db, id) | |
| 286 | + .await | |
| 287 | + .map(|()| Some("✓ removed".to_string())), | |
| 288 | + }; | |
| 289 | + match done { | |
| 290 | + Ok(msg) => app.status = msg.or(app.status.take()), | |
| 291 | + Err(err) => app.status = Some(format!("⚠ {}", err)), | |
| 273 | 292 | } |
| 293 | + app.notes = NoteOps::list(&db).await?; | |
| 294 | + app.note_popup = Some(popup); | |
| 274 | 295 | } |
| 275 | 296 | Event::Key(key_event) |
| 276 | 297 | if key_event.kind == KeyEventKind::Press && app.calendar_menu.is_some() => |
@@ -406,7 +427,7 @@ pub async fn run_tui(
| 406 | 427 | false => app.ppl_state.scroll_down_by(1), |
| 407 | 428 | }, |
| 408 | 429 | Tabs::Calendar => match app.sigdate_cal_state.selected() { |
| 409 | − Some(i) if i.saturating_add(1) >= app.upcoming_holidays.len() => { | |
| 430 | + Some(i) if i.saturating_add(1) >= app.upcoming_rows.len() => { | |
| 410 | 431 | app.set_cal_year(app.cal_year + 1, Some(0)) |
| 411 | 432 | } |
| 412 | 433 | _ => app.sigdate_cal_state.scroll_down_by(1), |
@@ -479,17 +500,12 @@ pub async fn run_tui(
| 479 | 500 | KeyCode::Char('i') => match app.current_tab { |
| 480 | 501 | Tabs::Ppl => app.ppl_show_tiers = !app.ppl_show_tiers, |
| 481 | 502 | Tabs::Calendar => { |
| 482 | − let selected = app | |
| 503 | + if let Some((occ, h)) = app | |
| 483 | 504 | .sigdate_cal_state |
| 484 | 505 | .selected() |
| 485 | − .and_then(|idx| app.upcoming_holidays.get(idx)); | |
| 486 | − match selected { | |
| 487 | − Some(Some(h)) => app.holiday_info = Some(h.clone()), | |
| 488 | − Some(None) => { | |
| 489 | − app.status = | |
| 490 | − Some("ℹ i tells you about built-in holidays".to_string()) | |
| 491 | − } | |
| 492 | − None => {} | |
| 506 | + .and_then(|idx| app.upcoming_rows.get(idx)) | |
| 507 | + { | |
| 508 | + app.note_popup = Some(NotePopup::new(occ.clone(), h.clone())); | |
| 493 | 509 | } |
| 494 | 510 | } |
| 495 | 511 | _ => {} |
@@ -1605,13 +1621,17 @@ fn render(f: &mut Frame, app: &mut Tui) {
| 1605 | 1621 | let mut upcoming = as_upcoming |
| 1606 | 1622 | .iter() |
| 1607 | 1623 | .filter(|_| show_people) |
| 1608 | − .map(|u| (u.date.date, None, ListItem::from(u))) | |
| 1624 | + .map(|u| { | |
| 1625 | + let occ = Occurrence::of_person(&u.date, u.date.date, &u.ppl); | |
| 1626 | + let item = with_badge(person_line(u), &occ, &app.notes); | |
| 1627 | + (u.date.date, (occ, None), item) | |
| 1628 | + }) | |
| 1609 | 1629 | .collect::<Vec<_>>(); |
| 1610 | − upcoming.extend( | |
| 1611 | − shown | |
| 1612 | − .iter() | |
| 1613 | − .map(|h| (h.date, Some((*h).clone()), holiday_item(h))), | |
| 1614 | − ); | |
| 1630 | + upcoming.extend(shown.iter().map(|h| { | |
| 1631 | + let occ = Occurrence::of_holiday(h); | |
| 1632 | + let item = with_badge(holiday_line(h), &occ, &app.notes); | |
| 1633 | + (h.date, (occ, Some((*h).clone())), item) | |
| 1634 | + })); | |
| 1615 | 1635 | upcoming.sort_by_key(|(d, _, _)| *d); |
| 1616 | 1636 | // Open on the first date from today on, with a couple of past ones above it |
| 1617 | 1637 | // (the first date of a later year, the last of an earlier one) |
@@ -1630,8 +1650,8 @@ fn render(f: &mut Frame, app: &mut Tui) {
| 1630 | 1650 | app.sigdate_cal_state.select(Some(upcoming.len() - 1)); |
| 1631 | 1651 | } |
| 1632 | 1652 | } |
| 1633 | − // Remember which rows are holidays, for `i` | |
| 1634 | − app.upcoming_holidays = upcoming.iter().map(|(_, h, _)| h.clone()).collect(); | |
| 1653 | + // Remember what each row is, for `i` | |
| 1654 | + app.upcoming_rows = upcoming.iter().map(|(_, row, _)| row.clone()).collect(); | |
| 1635 | 1655 | |
| 1636 | 1656 | // The selected row's day stands out in the grid, so scrolling shows where you are |
| 1637 | 1657 | let selected_day = app |
@@ -1802,8 +1822,8 @@ fn render(f: &mut Frame, app: &mut Tui) {
| 1802 | 1822 | if let Some(menu) = app.calendar_menu.as_mut() { |
| 1803 | 1823 | menu.render(f, &app.calendars_enabled, &app.calendar_rows); |
| 1804 | 1824 | } |
| 1805 | − if let Some(h) = &app.holiday_info { | |
| 1806 | − render_holiday_info(f, h); | |
| 1825 | + if let Some(popup) = &app.note_popup { | |
| 1826 | + popup.render(f, &app.notes); | |
| 1807 | 1827 | } |
| 1808 | 1828 | if let Some(form) = &app.add_form { |
| 1809 | 1829 | let subtitle = match form.ppl_id { |
@@ -1882,6 +1902,13 @@ impl From<PplAndProps> for ListItem<'_> {
| 1882 | 1902 | |
| 1883 | 1903 | impl From<&SigDateAndProps> for ListItem<'_> { |
| 1884 | 1904 | fn from(value: &SigDateAndProps) -> Self { |
| 1905 | + ListItem::new(person_line(value)) | |
| 1906 | + } | |
| 1907 | +} | |
| 1908 | + | |
| 1909 | +/// A person's date in the upcoming list | |
| 1910 | +fn person_line(value: &SigDateAndProps) -> Line<'static> { | |
| 1911 | + { | |
| 1885 | 1912 | let n = value.ppl.nick.clone().unwrap_or(value.ppl.name.clone()); |
| 1886 | 1913 | let default = value |
| 1887 | 1914 | .trait_defaults |
@@ -1907,16 +1934,24 @@ impl From<&SigDateAndProps> for ListItem<'_> {
| 1907 | 1934 | ), |
| 1908 | 1935 | }; |
| 1909 | 1936 | |
| 1910 | − ListItem::new(line) | |
| 1937 | + line | |
| 1911 | 1938 | } |
| 1912 | 1939 | } |
| 1913 | 1940 | |
| 1914 | −fn holiday_item(h: &Holiday) -> ListItem<'static> { | |
| 1915 | − ListItem::new(Line::from(vec![ | |
| 1941 | +fn holiday_line(h: &Holiday) -> Line<'static> { | |
| 1942 | + Line::from(vec![ | |
| 1916 | 1943 | Span::styled(" ◆ ", holiday_color(h)), |
| 1917 | 1944 | Span::styled(format!("{} {}", h.date, h.name), WHITE), |
| 1918 | 1945 | Span::styled(format!(" {}", h.calendar.key), SLATE.c500), |
| 1919 | − ])) | |
| 1946 | + ]) | |
| 1947 | +} | |
| 1948 | + | |
| 1949 | +/// An upcoming row, with its notes' badge (open todos, or 📝) at the end | |
| 1950 | +fn with_badge(mut line: Line<'static>, occ: &Occurrence, all: &[note::Model]) -> ListItem<'static> { | |
| 1951 | + if let Some(b) = notes::badge(&occ.notes(all)) { | |
| 1952 | + line.push_span(Span::styled(format!(" {}", b), ORANGE.c400)); | |
| 1953 | + } | |
| 1954 | + ListItem::new(line) | |
| 1920 | 1955 | } |
| 1921 | 1956 | |
| 1922 | 1957 | impl From<&ppl::Model> for ListItem<'_> { |
modified
src/entities/mod.rs
+1
−0
@@ -5,6 +5,7 @@ pub mod prelude;
| 5 | 5 | pub mod calendar; |
| 6 | 6 | pub mod contact; |
| 7 | 7 | pub mod entitys; |
| 8 | +pub mod note; | |
| 8 | 9 | pub mod ppl; |
| 9 | 10 | pub mod relation; |
| 10 | 11 | pub mod sig_date; |
added
src/entities/note.rs
+27
−0
@@ -0,0 +1,27 @@
| 1 | +use sea_orm::entity::prelude::*; | |
| 2 | + | |
| 3 | +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] | |
| 4 | +#[sea_orm(table_name = "note")] | |
| 5 | +pub struct Model { | |
| 6 | + #[sea_orm(primary_key)] | |
| 7 | + pub id: i32, | |
| 8 | + /// "note" or "todo" | |
| 9 | + pub kind: String, | |
| 10 | + pub text: String, | |
| 11 | + /// For todos | |
| 12 | + pub done: bool, | |
| 13 | + /// The day of the occurrence it's on | |
| 14 | + pub date: Date, | |
| 15 | + /// For a holiday or iCal event: its calendar's key and its name | |
| 16 | + pub calendar: Option<String>, | |
| 17 | + pub event: Option<String>, | |
| 18 | + /// For a person's date | |
| 19 | + pub sig_date_id: Option<i32>, | |
| 20 | + pub date_ins: Date, | |
| 21 | + pub date_up: Date, | |
| 22 | +} | |
| 23 | + | |
| 24 | +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | |
| 25 | +pub enum Relation {} | |
| 26 | + | |
| 27 | +impl ActiveModelBehavior for ActiveModel {} |
modified
src/entities/prelude.rs
+1
−0
Click to load diff…
modified
src/holidays.rs
+4
−1
Click to load diff…
modified
src/ical.rs
+18
−10
Click to load diff…
modified
src/main.rs
+107
−3
Click to load diff…
added
src/migrator/m20261019_000016_create_note.rs
+64
−0
Click to load diff…
modified
src/migrator/mod.rs
+2
−0
Click to load diff…
added
src/notes.rs
+274
−0
Click to load diff…
modified
src/tui_calendars.rs
+0
−42
Click to load diff…
added
src/tui_notes.rs
+206
−0
Click to load diff…
Parents: 0f89366