diff --git a/src/cli.rs b/src/cli.rs index a01fe77..e2027f8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,11 +8,8 @@ use structopt::StructOpt; use crate::eval::{DateRange, EntryMode}; use crate::files::Files; -use self::layout::Layout; -use self::render::Render; - mod layout; -mod render; +mod show; #[derive(Debug, StructOpt)] pub struct Opt { @@ -67,16 +64,11 @@ pub fn run() -> anyhow::Result<()> { .expect("determine range"); let entries = files.eval(EntryMode::Relevant, range)?; - - let mut layout = Layout::new(range, now); - layout.layout(&files, &entries); - - let mut render = Render::new(); - render.render(&files, &entries, &layout); + let layout = layout::layout(&files, &entries, range, now); match opt.command { None | Some(Command::Show) => match opt.entry { - None => print!("{}", render.display()), + None => print!("{}", show::show_all(&layout)), // Some(i) => print!("{}", render::render_entry(&files, &entries, &layout, i)), Some(i) => todo!(), }, diff --git a/src/cli/layout.rs b/src/cli/layout.rs index 0da4358..76575eb 100644 --- a/src/cli/layout.rs +++ b/src/cli/layout.rs @@ -1,238 +1,25 @@ -use std::collections::HashMap; +use chrono::NaiveDateTime; -use chrono::{NaiveDate, NaiveDateTime}; - -use crate::eval::{DateRange, Dates, Entry, EntryKind}; -use crate::files::commands::Command; -use crate::files::primitives::Time; +use crate::eval::{DateRange, Entry}; use crate::files::Files; -#[derive(Debug)] -pub enum LayoutEntry { - End(usize), - Now(Time), - TimedEnd(usize, Time), - TimedAt(usize, Time), - TimedStart(usize, Time), - ReminderSince(usize, i64), - At(usize), - ReminderWhile(usize, i64), - Undated(usize), - Start(usize), - ReminderUntil(usize, i64), -} - -#[derive(Debug)] -pub struct Layout { - pub range: DateRange, - pub today: NaiveDate, - pub time: Time, - pub earlier: Vec, - pub days: HashMap>, -} - -impl Layout { - pub fn new(range: DateRange, now: NaiveDateTime) -> Self { - Self { - range, - today: now.date(), - time: now.time().into(), - earlier: vec![], - days: range.days().map(|d| (d, vec![])).collect(), - } - } - - pub fn layout(&mut self, files: &Files, entries: &[Entry]) { - self.insert(self.today, LayoutEntry::Now(self.time)); - - let mut commands = entries - .iter() - .enumerate() - .map(|(i, e)| (i, e, files.command(e.source))) - .collect::>(); - - Self::sort_entries(&mut commands); - - for (index, entry, _) in commands { - self.layout_entry(index, entry); - } - - for (_, day) in self.days.iter_mut() { - Self::sort_day(day); - } - - // TODO Combine TimedStart and TimedEnd if there is nothing in-between - } - - fn layout_entry(&mut self, index: usize, entry: &Entry) { - match entry.kind { - EntryKind::Task => self.layout_task(index, entry), - EntryKind::TaskDone(at) => self.layout_task_done(index, entry, at), - EntryKind::Note | EntryKind::Birthday(_) => self.layout_note(index, entry), - } - } - - fn layout_task(&mut self, index: usize, entry: &Entry) { - if let Some(dates) = entry.dates { - let (start, end) = dates.start_end(); - if self.today < start && (start - self.today).num_days() < 7 { - // TODO Make this adjustable, maybe even per-command - let days = (start - self.today).num_days(); - self.insert(self.today, LayoutEntry::ReminderUntil(index, days)); - } else if start < self.today && self.today < end { - let days = (end - self.today).num_days(); - self.insert(self.today, LayoutEntry::ReminderWhile(index, days)); - } else if end < self.today { - let days = (self.today - end).num_days(); - self.insert(self.today, LayoutEntry::ReminderSince(index, days)); - } - self.layout_dated_entry(index, dates); - } else { - self.insert(self.today, LayoutEntry::Undated(index)); - } - } - - fn layout_task_done(&mut self, index: usize, entry: &Entry, at: NaiveDate) { - if let Some(dates) = entry.dates { - if at > dates.end() { - let days = (at - dates.end()).num_days(); - self.insert(at, LayoutEntry::ReminderSince(index, days)); - } - self.layout_dated_entry(index, dates); - } else { - // Treat the task as if its date was its completion time - self.layout_dated_entry(index, Dates::new(at, at)); - } - } - - fn layout_note(&mut self, index: usize, entry: &Entry) { - if let Some(dates) = entry.dates { - let (start, end) = dates.start_end(); - if start < self.range.from() && self.range.until() < end { - // This note applies to the current day, but it won't appear if - // we just layout it as a dated entry, so instead we add it as a - // reminder. Since we are usually more interested in when - // something ends than when it starts, we count the days until - // the end. - let days = (end - self.today).num_days(); - self.insert(self.today, LayoutEntry::ReminderWhile(index, days)); - } else { - self.layout_dated_entry(index, dates); - } - } else { - self.insert(self.today, LayoutEntry::Undated(index)); - } - } - - fn layout_dated_entry(&mut self, index: usize, dates: Dates) { - let (start, end) = dates.start_end(); - if let Some((date, time)) = dates.point_in_time() { - let entry = match time { - Some(time) => LayoutEntry::TimedAt(index, time), - None => LayoutEntry::At(index), - }; - self.insert(date, entry); - } else if start < self.range.from() && self.range.until() < end { - // Neither the start nor end layout entries would be visible - // directly. However, the start layout entry would be added to - // [`self.earlier`]. Since [`self.earlier`] only exists so that - // every end entry has a corresponding start entry (for rendering), - // this would be pointless, so we don't add any entries. - } else { - let (start_entry, end_entry) = match dates.start_end_time() { - Some((start_time, end_time)) => ( - LayoutEntry::TimedStart(index, start_time), - LayoutEntry::TimedEnd(index, end_time), - ), - None => (LayoutEntry::Start(index), LayoutEntry::End(index)), - }; - self.insert(start, start_entry); - self.insert(end, end_entry); - } - } - - fn insert(&mut self, date: NaiveDate, e: LayoutEntry) { - if date < self.range.from() { - self.earlier.push(e); - } else if let Some(es) = self.days.get_mut(&date) { - es.push(e); - } - } - - fn sort_entries(entries: &mut Vec<(usize, &Entry, &Command)>) { - // Entries should be sorted by these factors, in descending order of - // significance: - // 1. Their start date, if any - // 2. Their end date, if any - // 3. Their kind - // 4. Their title - - // 4. - entries.sort_by_key(|(_, _, c)| c.title()); - - // 3. - entries.sort_by_key(|(_, e, _)| match e.kind { - EntryKind::Task => 0, - EntryKind::TaskDone(_) => 1, - EntryKind::Birthday(_) => 2, - EntryKind::Note => 3, - }); - - // 2. - entries.sort_by_key(|(_, e, _)| e.dates.map(|d| (d.end(), d.end_time()))); - - // 1. - entries.sort_by_key(|(_, e, _)| e.dates.map(|d| (d.start(), d.start_time()))); - } - - fn sort_day(day: &mut Vec) { - // In a day, entries should be sorted into these categories: - // 1. Untimed entries that end at the current day - // 2. Timed entries, based on - // 2.1. Their time - // 2.2. Their type (ending, at, starting) - // 3. Reminders for overdue entries - // 4. Untimed entries occurring today - // 5. Reminders for entries ending soon - // 6. Undated entries occurring today - // 7. Untimed entries starting today - // 8. Reminders for entries starting soon - // - // Entries within a single category should already be ordered based on - // their kind and title since the order they are layouted in takes these - // into account. - - // Ensure timed entries for a single time occur in the correct order - day.sort_by_key(|e| match e { - LayoutEntry::Now(_) => 1, - LayoutEntry::TimedEnd(_, _) => 2, - LayoutEntry::TimedAt(_, _) => 3, - LayoutEntry::TimedStart(_, _) => 4, - _ => 0, - }); - - // Ensure timed entries for different times occur in the correct order - day.sort_by_key(|e| match e { - LayoutEntry::Now(t) => Some(*t), - LayoutEntry::TimedEnd(_, t) => Some(*t), - LayoutEntry::TimedAt(_, t) => Some(*t), - LayoutEntry::TimedStart(_, t) => Some(*t), - _ => None, - }); - - // Ensure categories occur in the correct order - day.sort_by_key(|e| match e { - LayoutEntry::End(_) => 0, - LayoutEntry::Now(_) => 1, - LayoutEntry::TimedEnd(_, _) => 1, - LayoutEntry::TimedAt(_, _) => 1, - LayoutEntry::TimedStart(_, _) => 1, - LayoutEntry::ReminderSince(_, _) => 2, - LayoutEntry::At(_) => 3, - LayoutEntry::ReminderWhile(_, _) => 4, - LayoutEntry::Undated(_) => 5, - LayoutEntry::Start(_) => 6, - LayoutEntry::ReminderUntil(_, _) => 7, - }) - } +use self::day::DayLayout; +use self::line::LineLayout; + +mod day; +pub mod line; + +pub fn layout( + files: &Files, + entries: &[Entry], + range: DateRange, + now: NaiveDateTime, +) -> LineLayout { + let mut day_layout = DayLayout::new(range, now); + day_layout.layout(files, entries); + + let mut line_layout = LineLayout::new(); + line_layout.render(files, entries, &day_layout); + + line_layout } diff --git a/src/cli/layout/day.rs b/src/cli/layout/day.rs new file mode 100644 index 0000000..9bd3363 --- /dev/null +++ b/src/cli/layout/day.rs @@ -0,0 +1,243 @@ +//! Organize a list of entries into a specified range of days. +//! +//! This includes adding reminders and ordering everything so it will be nicer +//! to display later. + +use std::collections::HashMap; + +use chrono::{NaiveDate, NaiveDateTime}; + +use crate::eval::{DateRange, Dates, Entry, EntryKind}; +use crate::files::commands::Command; +use crate::files::primitives::Time; +use crate::files::Files; + +#[derive(Debug)] +pub enum DayEntry { + End(usize), + Now(Time), + TimedEnd(usize, Time), + TimedAt(usize, Time), + TimedStart(usize, Time), + ReminderSince(usize, i64), + At(usize), + ReminderWhile(usize, i64), + Undated(usize), + Start(usize), + ReminderUntil(usize, i64), +} + +#[derive(Debug)] +pub struct DayLayout { + pub range: DateRange, + pub today: NaiveDate, + pub time: Time, + pub earlier: Vec, + pub days: HashMap>, +} + +impl DayLayout { + pub fn new(range: DateRange, now: NaiveDateTime) -> Self { + Self { + range, + today: now.date(), + time: now.time().into(), + earlier: vec![], + days: range.days().map(|d| (d, vec![])).collect(), + } + } + + pub fn layout(&mut self, files: &Files, entries: &[Entry]) { + self.insert(self.today, DayEntry::Now(self.time)); + + let mut commands = entries + .iter() + .enumerate() + .map(|(i, e)| (i, e, files.command(e.source))) + .collect::>(); + + Self::sort_entries(&mut commands); + + for (index, entry, _) in commands { + self.layout_entry(index, entry); + } + + for (_, day) in self.days.iter_mut() { + Self::sort_day(day); + } + + // TODO Combine TimedStart and TimedEnd if there is nothing in-between + } + + fn layout_entry(&mut self, index: usize, entry: &Entry) { + match entry.kind { + EntryKind::Task => self.layout_task(index, entry), + EntryKind::TaskDone(at) => self.layout_task_done(index, entry, at), + EntryKind::Note | EntryKind::Birthday(_) => self.layout_note(index, entry), + } + } + + fn layout_task(&mut self, index: usize, entry: &Entry) { + if let Some(dates) = entry.dates { + let (start, end) = dates.start_end(); + if self.today < start && (start - self.today).num_days() < 7 { + // TODO Make this adjustable, maybe even per-command + let days = (start - self.today).num_days(); + self.insert(self.today, DayEntry::ReminderUntil(index, days)); + } else if start < self.today && self.today < end { + let days = (end - self.today).num_days(); + self.insert(self.today, DayEntry::ReminderWhile(index, days)); + } else if end < self.today { + let days = (self.today - end).num_days(); + self.insert(self.today, DayEntry::ReminderSince(index, days)); + } + self.layout_dated_entry(index, dates); + } else { + self.insert(self.today, DayEntry::Undated(index)); + } + } + + fn layout_task_done(&mut self, index: usize, entry: &Entry, at: NaiveDate) { + if let Some(dates) = entry.dates { + if at > dates.end() { + let days = (at - dates.end()).num_days(); + self.insert(at, DayEntry::ReminderSince(index, days)); + } + self.layout_dated_entry(index, dates); + } else { + // Treat the task as if its date was its completion time + self.layout_dated_entry(index, Dates::new(at, at)); + } + } + + fn layout_note(&mut self, index: usize, entry: &Entry) { + if let Some(dates) = entry.dates { + let (start, end) = dates.start_end(); + if start < self.range.from() && self.range.until() < end { + // This note applies to the current day, but it won't appear if + // we just layout it as a dated entry, so instead we add it as a + // reminder. Since we are usually more interested in when + // something ends than when it starts, we count the days until + // the end. + let days = (end - self.today).num_days(); + self.insert(self.today, DayEntry::ReminderWhile(index, days)); + } else { + self.layout_dated_entry(index, dates); + } + } else { + self.insert(self.today, DayEntry::Undated(index)); + } + } + + fn layout_dated_entry(&mut self, index: usize, dates: Dates) { + let (start, end) = dates.start_end(); + if let Some((date, time)) = dates.point_in_time() { + let entry = match time { + Some(time) => DayEntry::TimedAt(index, time), + None => DayEntry::At(index), + }; + self.insert(date, entry); + } else if start < self.range.from() && self.range.until() < end { + // Neither the start nor end layout entries would be visible + // directly. However, the start layout entry would be added to + // [`self.earlier`]. Since [`self.earlier`] only exists so that + // every end entry has a corresponding start entry (for rendering), + // this would be pointless, so we don't add any entries. + } else { + let (start_entry, end_entry) = match dates.start_end_time() { + Some((start_time, end_time)) => ( + DayEntry::TimedStart(index, start_time), + DayEntry::TimedEnd(index, end_time), + ), + None => (DayEntry::Start(index), DayEntry::End(index)), + }; + self.insert(start, start_entry); + self.insert(end, end_entry); + } + } + + fn insert(&mut self, date: NaiveDate, e: DayEntry) { + if date < self.range.from() { + self.earlier.push(e); + } else if let Some(es) = self.days.get_mut(&date) { + es.push(e); + } + } + + fn sort_entries(entries: &mut Vec<(usize, &Entry, &Command)>) { + // Entries should be sorted by these factors, in descending order of + // significance: + // 1. Their start date, if any + // 2. Their end date, if any + // 3. Their kind + // 4. Their title + + // 4. + entries.sort_by_key(|(_, _, c)| c.title()); + + // 3. + entries.sort_by_key(|(_, e, _)| match e.kind { + EntryKind::Task => 0, + EntryKind::TaskDone(_) => 1, + EntryKind::Birthday(_) => 2, + EntryKind::Note => 3, + }); + + // 2. + entries.sort_by_key(|(_, e, _)| e.dates.map(|d| (d.end(), d.end_time()))); + + // 1. + entries.sort_by_key(|(_, e, _)| e.dates.map(|d| (d.start(), d.start_time()))); + } + + fn sort_day(day: &mut Vec) { + // In a day, entries should be sorted into these categories: + // 1. Untimed entries that end at the current day + // 2. Timed entries, based on + // 2.1. Their time + // 2.2. Their type (ending, at, starting) + // 3. Reminders for overdue entries + // 4. Untimed entries occurring today + // 5. Reminders for entries ending soon + // 6. Undated entries occurring today + // 7. Untimed entries starting today + // 8. Reminders for entries starting soon + // + // Entries within a single category should already be ordered based on + // their kind and title since the order they are layouted in takes these + // into account. + + // Ensure timed entries for a single time occur in the correct order + day.sort_by_key(|e| match e { + DayEntry::Now(_) => 1, + DayEntry::TimedEnd(_, _) => 2, + DayEntry::TimedAt(_, _) => 3, + DayEntry::TimedStart(_, _) => 4, + _ => 0, + }); + + // Ensure timed entries for different times occur in the correct order + day.sort_by_key(|e| match e { + DayEntry::Now(t) => Some(*t), + DayEntry::TimedEnd(_, t) => Some(*t), + DayEntry::TimedAt(_, t) => Some(*t), + DayEntry::TimedStart(_, t) => Some(*t), + _ => None, + }); + + // Ensure categories occur in the correct order + day.sort_by_key(|e| match e { + DayEntry::End(_) => 0, + DayEntry::Now(_) => 1, + DayEntry::TimedEnd(_, _) => 1, + DayEntry::TimedAt(_, _) => 1, + DayEntry::TimedStart(_, _) => 1, + DayEntry::ReminderSince(_, _) => 2, + DayEntry::At(_) => 3, + DayEntry::ReminderWhile(_, _) => 4, + DayEntry::Undated(_) => 5, + DayEntry::Start(_) => 6, + DayEntry::ReminderUntil(_, _) => 7, + }) + } +} diff --git a/src/cli/render.rs b/src/cli/layout/line.rs similarity index 58% rename from src/cli/render.rs rename to src/cli/layout/line.rs index d0a9b8f..714fb64 100644 --- a/src/cli/render.rs +++ b/src/cli/layout/line.rs @@ -1,22 +1,26 @@ -use std::cmp; +//! Organize layouted entries into a list of lines to display. +//! +//! Additional information, such as the mapping from numbers to entries, are +//! collected along the way. The lines are not yet rendered into strings. + use std::collections::HashMap; -use chrono::{Datelike, NaiveDate}; +use chrono::NaiveDate; use crate::eval::{Entry, EntryKind}; -use crate::files::primitives::{Time, Weekday}; +use crate::files::primitives::Time; use crate::files::Files; -use super::layout::{Layout, LayoutEntry}; +use super::day::{DayEntry, DayLayout}; #[derive(Debug, Clone, Copy)] -enum SpanSegment { +pub enum SpanSegment { Start, Middle, End, } -enum Line { +pub enum LineEntry { Day { spans: Vec>, date: NaiveDate, @@ -33,14 +37,20 @@ enum Line { }, } -pub struct Render { +pub struct LineLayout { + /// Map from entry indices to their corresponding display numbers. + /// + /// Display numbers start at 1, not 0. numbers: HashMap, + /// The last number that was used as a display number. + /// + /// Is set to 0 initially, which is fine since display numbers start at 1. last_number: usize, spans: Vec>, - lines: Vec, + lines: Vec, } -impl Render { +impl LineLayout { pub fn new() -> Self { Self { numbers: HashMap::new(), @@ -50,11 +60,11 @@ impl Render { } } - pub fn render(&mut self, files: &Files, entries: &[Entry], layout: &Layout) { + pub fn render(&mut self, files: &Files, entries: &[Entry], layout: &DayLayout) { // Make sure spans for visible `*End`s are drawn for entry in &layout.earlier { match entry { - LayoutEntry::TimedStart(i, _) | LayoutEntry::Start(i) => self.start_span(*i), + DayEntry::TimedStart(i, _) | DayEntry::Start(i) => self.start_span(*i), _ => {} } } @@ -62,7 +72,7 @@ impl Render { for day in layout.range.days() { let spans = self.spans_for_line(); - self.line(Line::Day { spans, date: day }); + self.line(LineEntry::Day { spans, date: day }); let layout_entries = layout.days.get(&day).expect("got nonexisting day"); for layout_entry in layout_entries { @@ -71,41 +81,56 @@ impl Render { } } - pub fn display(&self) -> String { - let num_width = format!("{}", self.last_number).len(); - let num_width = cmp::max(num_width, 3); // for a "now" in the first column - let span_width = self.spans.len(); - - let mut ctx = DisplayContext::new(num_width, span_width); - for line in &self.lines { - ctx.display_line(line); - } - - ctx.result() + pub fn num_width(&self) -> usize { + format!("{}", self.last_number).len() } - fn render_layout_entry(&mut self, files: &Files, entries: &[Entry], l_entry: &LayoutEntry) { + pub fn span_width(&self) -> usize { + self.spans.len() + } + + pub fn lines(&self) -> &[LineEntry] { + &self.lines + } + + /// Return a map from entry indices to their corresponding display numbers. + /// + /// If you need to resolve a display number into an entry index, use + /// [`look_up_number`] instead. + pub fn numbers(&self) -> &HashMap { + &self.numbers + } + + pub fn look_up_number(&self, number: usize) -> Option { + self.numbers + .iter() + .filter(|(_, n)| **n == number) + .map(|(i, _)| *i) + .next() + } + + fn render_layout_entry(&mut self, files: &Files, entries: &[Entry], l_entry: &DayEntry) { match l_entry { - LayoutEntry::End(i) => { + DayEntry::End(i) => { self.stop_span(*i); self.line_entry(Some(*i), None, Self::format_entry(files, entries, *i)); } - LayoutEntry::Now(t) => self.line(Line::Now { + DayEntry::Now(t) => self.line(LineEntry::Now { spans: self.spans_for_line(), time: *t, }), - LayoutEntry::TimedEnd(i, t) => { + DayEntry::TimedEnd(i, t) => { self.stop_span(*i); self.line_entry(Some(*i), Some(*t), Self::format_entry(files, entries, *i)); } - LayoutEntry::TimedAt(i, t) => { + DayEntry::TimedAt(i, t) => { self.line_entry(Some(*i), Some(*t), Self::format_entry(files, entries, *i)); } - LayoutEntry::TimedStart(i, t) => { + DayEntry::TimedStart(i, t) => { self.start_span(*i); self.line_entry(Some(*i), Some(*t), Self::format_entry(files, entries, *i)); } - LayoutEntry::ReminderSince(i, d) => { + DayEntry::ReminderSince(i, d) => { let text = Self::format_entry(files, entries, *i); let text = if *d == 1 { format!("{} (yesterday)", text) @@ -114,23 +139,23 @@ impl Render { }; self.line_entry(Some(*i), None, text); } - LayoutEntry::At(i) => { + DayEntry::At(i) => { self.line_entry(Some(*i), None, Self::format_entry(files, entries, *i)) } - LayoutEntry::ReminderWhile(i, d) => { + DayEntry::ReminderWhile(i, d) => { let text = Self::format_entry(files, entries, *i); let plural = if *d == 1 { "" } else { "s" }; let text = format!("{} ({} day{} left)", text, i, plural); self.line_entry(Some(*i), None, text); } - LayoutEntry::Undated(i) => { + DayEntry::Undated(i) => { self.line_entry(Some(*i), None, Self::format_entry(files, entries, *i)); } - LayoutEntry::Start(i) => { + DayEntry::Start(i) => { self.start_span(*i); self.line_entry(Some(*i), None, Self::format_entry(files, entries, *i)); } - LayoutEntry::ReminderUntil(i, d) => { + DayEntry::ReminderUntil(i, d) => { let text = Self::format_entry(files, entries, *i); let text = if *d == 1 { format!("{} (tomorrow)", text) @@ -192,7 +217,7 @@ impl Render { .collect() } - fn line(&mut self, line: Line) { + fn line(&mut self, line: LineEntry) { self.lines.push(line); self.step_spans(); } @@ -210,7 +235,7 @@ impl Render { None => None, }; - self.line(Line::Entry { + self.line(LineEntry::Entry { number, spans: self.spans_for_line(), time, @@ -218,108 +243,3 @@ impl Render { }); } } - -struct DisplayContext { - num_width: usize, - span_width: usize, - result: String, -} - -impl DisplayContext { - fn new(num_width: usize, span_width: usize) -> Self { - Self { - num_width, - span_width, - result: String::new(), - } - } - - fn display_line(&mut self, line: &Line) { - match line { - Line::Day { spans, date } => self.display_line_date(spans, *date), - Line::Now { spans, time } => self.display_line_now(spans, *time), - Line::Entry { - number, - spans, - time, - text, - } => self.display_line_entry(*number, spans, *time, text), - } - } - - fn display_line_date(&mut self, spans: &[Option], date: NaiveDate) { - let weekday: Weekday = date.weekday().into(); - let weekday = weekday.full_name(); - self.push(&format!( - "{:=>nw$}={:=nw$}\n", - "", - Self::display_spans(spans, '='), - weekday, - date, - "", - "", - nw = self.num_width, - sw = self.span_width - )); - } - - fn display_line_now(&mut self, spans: &[Option], time: Time) { - self.push(&format!( - "{:, - spans: &[Option], - time: Option