use std::convert::TryFrom; use std::io::{BufRead, Write}; use clap::ArgMatches; use chrono::{DateTime, Utc}; use crate::error::{Error, Result}; use crate::timeparse::parse_time; use crate::database::Database; use crate::io::Streams; use crate::interactive::note_from_last_entries; use super::{Command, Facts, r#in, sheet}; #[derive(Default)] enum SelectedEntry { Id(u64), Interactive, #[default] NotSpecified, } #[derive(Default)] pub struct Args { entry: SelectedEntry, at: Option>, } impl<'a> TryFrom<&'a ArgMatches> for Args { type Error = Error; fn try_from(matches: &'a ArgMatches) -> Result { let entry = if let Some(m) = matches.value_of("id") { SelectedEntry::Id(m.parse().unwrap()) } else if matches.is_present("interactive") { SelectedEntry::Interactive } else { SelectedEntry::NotSpecified }; Ok(Args { at: matches.value_of("at").map(parse_time).transpose()?, entry, }) } } fn resume(args: Args, streams: &mut Streams, facts: &Facts, note: String) -> Result<()> where D: Database, I: BufRead, O: Write, E: Write, { writeln!( streams.out, "Resuming \"{note}\"", )?; r#in::InCommand::handle(r#in::Args { at: args.at, note: Some(note), }, streams, facts) } pub struct ResumeCommand; impl<'a> Command<'a> for ResumeCommand { type Args = Args; fn handle(args: Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, I: BufRead, O: Write, E: Write, { let current_sheet = streams.db.current_sheet()?; // First try to process using the given id match args.entry { SelectedEntry::Id(entry_id) => { if let Some(entry) = streams.db.entry_by_id(entry_id)? { if entry.sheet != current_sheet { // first swith to the sheet sheet::SheetCommand::handle(sheet::Args { sheet: Some(entry.sheet.clone()), }, streams, facts)?; } resume(args, streams, facts, entry.note.unwrap_or_default()) } else { writeln!(streams.out, "The entry with id '{}' could not be found to be resumed. Perhaps it was deleted?", entry_id)?; Ok(()) } } SelectedEntry::Interactive => { if let Some(note) = note_from_last_entries(streams, facts, ¤t_sheet)? { resume(args, streams, facts, note) } else { Ok(()) } } SelectedEntry::NotSpecified => { // No id specified, try to find something suitable to switch to in the // database if let Some(entry) = streams.db.last_checkout_of_sheet(¤t_sheet)? { resume(args, streams, facts, entry.note.unwrap_or_default()) } else { writeln!(streams.out, "No entry to resume in the sheet '{}'. Perhaps start a new one? Hint: use t in", current_sheet)?; Ok(()) } } } } } #[cfg(test)] mod tests { use chrono::Duration; use pretty_assertions::{assert_eq, assert_str_eq}; use super::*; #[test] fn resume_an_entry() { let args = Default::default(); let mut streams = Streams::fake(b""); let facts = Facts::new(); let one_hour_ago = facts.now - Duration::hours(1); let two_hours_ago = facts.now - Duration::hours(2); streams.db.entry_insert(two_hours_ago, Some(one_hour_ago), Some("fake note".into()), "default").unwrap(); assert_eq!(streams.db.entries_full(None, None).unwrap().len(), 1); ResumeCommand::handle(args, &mut streams, &facts).unwrap(); let all_entries = streams.db.entries_full(None, None).unwrap(); assert_eq!(all_entries.len(), 2); assert_eq!(all_entries[1].id, 2); assert_eq!(all_entries[1].start, facts.now); assert_eq!(all_entries[1].end, None); assert_eq!(all_entries[1].note, Some("fake note".into())); assert_eq!(all_entries[1].sheet, "default"); assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Resuming \"fake note\" Checked into sheet \"default\".\n"); assert_str_eq!(&String::from_utf8_lossy(&streams.err), ""); } #[test] fn no_entries_to_resume() { let args = Default::default(); let mut streams = Streams::fake(b""); let facts = Facts::new(); ResumeCommand::handle(args, &mut streams, &facts).unwrap(); assert_str_eq!(&String::from_utf8_lossy(&streams.out), "\ No entry to resume in the sheet 'default'. Perhaps start a new one? Hint: use t in "); } }