Skip to main content

doing/cli/commands/
on.rs

1use clap::Args;
2use doing_config::SortOrder;
3use doing_ops::filter::filter_entries;
4use doing_time::{chronify, parse_range};
5
6use crate::{
7  Result,
8  cli::{
9    AppContext,
10    args::{DisplayArgs, FilterArgs},
11    pager,
12  },
13};
14
15/// Show entries from a specific date or date range.
16///
17/// Accepts a natural language date expression. If the expression contains
18/// a range separator (`to`, `through`, `thru`, `until`, `til`, `--`),
19/// entries within that range are shown. Otherwise, entries from that
20/// single date are shown.
21///
22/// # Examples
23///
24/// ```text
25/// doing on friday                 # entries from most recent friday
26/// doing on "last friday"          # entries from last friday
27/// doing on "3/15 to 3/20"        # entries within a date range
28/// doing on "2024-01-15"           # entries from a specific date
29/// doing on monday                 # entries from most recent monday
30/// ```
31#[derive(Args, Clone, Debug)]
32pub struct Command {
33  /// Date or date range expression (e.g. "last friday", "3/15 to 3/20")
34  #[arg(index = 1, required = true, value_name = "DATE")]
35  date: String,
36
37  #[command(flatten)]
38  display: DisplayArgs,
39
40  #[command(flatten)]
41  filter: FilterArgs,
42
43  /// Use a pager for output
44  #[arg(short, long)]
45  pager: bool,
46}
47
48impl Command {
49  pub fn call(&self, ctx: &mut AppContext) -> Result<()> {
50    let section_name = self.filter.section.as_deref().unwrap_or("all");
51
52    let all_entries: Vec<_> = ctx
53      .document
54      .entries_in_section(section_name)
55      .into_iter()
56      .cloned()
57      .collect();
58
59    let mut options = self
60      .filter
61      .clone()
62      .into_filter_options(&ctx.config, ctx.include_notes)?;
63    options.section = Some(section_name.to_string());
64
65    match parse_range(&self.date) {
66      Ok((start, end)) => {
67        if options.after.is_none() {
68          options.after = Some(start);
69        }
70        if options.before.is_none() {
71          options.before = Some(end);
72        }
73      }
74      Err(_) => {
75        let date = chronify(&self.date)?;
76        let day_start = date
77          .date_naive()
78          .and_hms_opt(0, 0, 0)
79          .and_then(|dt| dt.and_local_timezone(chrono::Local).single());
80        let day_end = date
81          .date_naive()
82          .and_hms_opt(23, 59, 59)
83          .and_then(|dt| dt.and_local_timezone(chrono::Local).single());
84
85        if options.after.is_none() {
86          options.after = day_start;
87        }
88        if options.before.is_none() {
89          options.before = day_end;
90        }
91      }
92    }
93
94    let sort_order = self.display.sort.map(SortOrder::from).or(Some(ctx.config.order));
95    options.sort = sort_order;
96
97    let filtered = filter_entries(all_entries, &options);
98
99    let output = self
100      .display
101      .render_entries(&filtered, &ctx.config, "default", ctx.include_notes)?;
102
103    if !output.is_empty() {
104      pager::output(&output, &ctx.config, self.pager || ctx.use_pager)?;
105    }
106
107    Ok(())
108  }
109}
110
111#[cfg(test)]
112mod test {
113  use chrono::{Duration, Local, TimeZone};
114  use doing_taskpaper::{Document, Entry, Note, Section, Tag, Tags};
115
116  use super::*;
117
118  fn default_cmd(date: &str) -> Command {
119    Command {
120      date: date.into(),
121      display: DisplayArgs::default(),
122      filter: FilterArgs::default(),
123      pager: false,
124    }
125  }
126
127  fn sample_ctx() -> AppContext {
128    let yesterday = Local::now() - Duration::days(1);
129    let mut doc = Document::new();
130    let mut section = Section::new("Currently");
131    section.add_entry(Entry::new(
132      yesterday,
133      "Yesterday's work",
134      Tags::from_iter(vec![Tag::new("coding", None::<String>)]),
135      Note::new(),
136      "Currently",
137      None::<String>,
138    ));
139    section.add_entry(Entry::new(
140      Local.with_ymd_and_hms(2024, 3, 15, 10, 0, 0).unwrap(),
141      "Old entry",
142      Tags::new(),
143      Note::new(),
144      "Currently",
145      None::<String>,
146    ));
147    doc.add_section(section);
148
149    AppContext {
150      config: doing_config::Config::default(),
151      default_answer: false,
152      document: doc,
153      doing_file: std::path::PathBuf::from("/tmp/test_doing.md"),
154      include_notes: true,
155      no: false,
156      noauto: false,
157      quiet: false,
158      stdout: false,
159      use_color: false,
160      use_pager: false,
161      yes: false,
162    }
163  }
164
165  mod call {
166    use super::*;
167
168    #[test]
169    fn it_returns_ok_with_single_date() {
170      let mut ctx = sample_ctx();
171      let cmd = default_cmd("yesterday");
172
173      let result = cmd.call(&mut ctx);
174
175      assert!(result.is_ok());
176    }
177
178    #[test]
179    fn it_returns_ok_with_date_range() {
180      let mut ctx = sample_ctx();
181      let cmd = default_cmd("2024-03-01 to 2024-03-31");
182
183      let result = cmd.call(&mut ctx);
184
185      assert!(result.is_ok());
186    }
187
188    #[test]
189    fn it_filters_by_section() {
190      let mut ctx = sample_ctx();
191      let cmd = Command {
192        filter: FilterArgs {
193          section: Some("Currently".into()),
194          ..FilterArgs::default()
195        },
196        ..default_cmd("yesterday")
197      };
198
199      let result = cmd.call(&mut ctx);
200
201      assert!(result.is_ok());
202    }
203
204    #[test]
205    fn it_handles_empty_document() {
206      let mut ctx = AppContext {
207        config: doing_config::Config::default(),
208        default_answer: false,
209        document: Document::new(),
210        doing_file: std::path::PathBuf::from("/tmp/test_doing.md"),
211        include_notes: true,
212        no: false,
213        noauto: false,
214        quiet: false,
215        stdout: false,
216        use_color: false,
217        use_pager: false,
218        yes: false,
219      };
220      let cmd = default_cmd("yesterday");
221
222      let result = cmd.call(&mut ctx);
223
224      assert!(result.is_ok());
225    }
226
227    #[test]
228    fn it_rejects_invalid_date() {
229      let mut ctx = sample_ctx();
230      let cmd = default_cmd("not a real date");
231
232      let result = cmd.call(&mut ctx);
233
234      assert!(result.is_err());
235    }
236  }
237}