2021-08-25 14:30:27 -05:00
|
|
|
use std::io::{self, BufRead, Write};
|
2022-07-21 08:36:05 -05:00
|
|
|
use std::collections::HashMap;
|
2022-07-21 08:54:43 -05:00
|
|
|
use std::cmp::Reverse;
|
2021-08-11 20:24:42 -05:00
|
|
|
|
2022-07-21 08:36:05 -05:00
|
|
|
use chrono::{DateTime, Utc, Duration};
|
2022-05-07 22:37:05 -05:00
|
|
|
|
2021-08-25 14:30:27 -05:00
|
|
|
use crate::io::Streams;
|
|
|
|
use crate::database::Database;
|
2022-05-07 10:10:38 -05:00
|
|
|
use crate::error::Result;
|
|
|
|
use crate::commands::Facts;
|
2022-05-07 22:37:05 -05:00
|
|
|
use crate::models::Entry;
|
2022-07-21 08:36:05 -05:00
|
|
|
use crate::tabulate::{Tabulate, Col, Align};
|
|
|
|
use crate::formatters::text::format_duration;
|
2021-08-25 14:30:27 -05:00
|
|
|
|
|
|
|
fn read_line<I: BufRead>(mut r#in: I) -> io::Result<String> {
|
2021-08-11 20:24:42 -05:00
|
|
|
let mut pre_n = String::new();
|
2021-08-25 14:30:27 -05:00
|
|
|
r#in.read_line(&mut pre_n)?;
|
2021-08-11 20:24:42 -05:00
|
|
|
Ok(pre_n)
|
|
|
|
}
|
|
|
|
|
2022-05-07 10:10:38 -05:00
|
|
|
pub fn ask<D, I, O, E>(streams: &mut Streams<D, I, O, E>, question: &str) -> io::Result<bool>
|
|
|
|
where
|
|
|
|
D: Database,
|
|
|
|
I: BufRead,
|
|
|
|
O: Write,
|
|
|
|
E: Write,
|
|
|
|
{
|
2021-08-25 14:30:27 -05:00
|
|
|
write!(streams.out, "{} [y/N] ", question)?;
|
|
|
|
streams.out.flush()?;
|
2021-08-11 20:24:42 -05:00
|
|
|
|
2021-08-25 14:30:27 -05:00
|
|
|
Ok(read_line(&mut streams.r#in)?.to_lowercase().starts_with('y'))
|
2021-08-11 20:24:42 -05:00
|
|
|
}
|
2022-05-07 10:10:38 -05:00
|
|
|
|
|
|
|
enum Choice {
|
|
|
|
Number(usize),
|
|
|
|
Quit,
|
|
|
|
CtrlD,
|
|
|
|
Whatever,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_choice(s: String) -> Choice {
|
|
|
|
let s = s.trim();
|
|
|
|
|
|
|
|
if let Ok(n) = s.parse::<usize>() {
|
|
|
|
if n == 0 {
|
|
|
|
Choice::Whatever
|
|
|
|
} else {
|
|
|
|
Choice::Number(n)
|
|
|
|
}
|
2022-05-07 10:37:30 -05:00
|
|
|
} else if s.is_empty() {
|
2022-05-07 10:10:38 -05:00
|
|
|
Choice::CtrlD
|
|
|
|
} else if s.to_lowercase() == "q" {
|
|
|
|
Choice::Quit
|
|
|
|
} else {
|
|
|
|
Choice::Whatever
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Offers the last N entries (configurable) to the user and waits for a choice.
|
|
|
|
pub fn note_from_last_entries<D, I, O, E>(streams: &mut Streams<D, I, O, E>, facts: &Facts, current_sheet: &str) -> Result<Option<String>>
|
|
|
|
where
|
|
|
|
D: Database,
|
|
|
|
I: BufRead,
|
|
|
|
O: Write,
|
|
|
|
E: Write,
|
|
|
|
{
|
|
|
|
let entries = streams.db.entries_by_sheet(current_sheet, None, None)?;
|
|
|
|
let mut uniques = HashMap::new();
|
|
|
|
|
2022-07-21 08:36:05 -05:00
|
|
|
struct GroupedEntry {
|
|
|
|
note: String,
|
|
|
|
last_start: DateTime<Utc>,
|
|
|
|
accumulated_time: Duration,
|
|
|
|
}
|
|
|
|
|
|
|
|
// From all possible entries belonging to this sheet keep only those with a
|
|
|
|
// note
|
|
|
|
let entries_with_notes = entries
|
|
|
|
// Iterate all entries
|
|
|
|
.into_iter()
|
|
|
|
// preserve only those with a text note
|
|
|
|
.filter_map(|e| e.note.map(|n| GroupedEntry {
|
|
|
|
note: n,
|
|
|
|
last_start: e.start,
|
|
|
|
accumulated_time: e.end.unwrap_or(facts.now) - e.start,
|
|
|
|
}));
|
|
|
|
|
|
|
|
// iterate over the entries with a note and group them into `uniques`
|
|
|
|
// accumulating their elapsed times and recording the last time it was
|
|
|
|
// started
|
|
|
|
for entry in entries_with_notes {
|
|
|
|
let mut e = uniques.entry(entry.note.clone()).or_insert(GroupedEntry {
|
|
|
|
accumulated_time: Duration::seconds(0),
|
|
|
|
..entry
|
|
|
|
});
|
|
|
|
|
|
|
|
if entry.last_start > e.last_start {
|
|
|
|
e.last_start = entry.last_start;
|
|
|
|
}
|
|
|
|
|
|
|
|
e.accumulated_time = e.accumulated_time + entry.accumulated_time;
|
|
|
|
}
|
2022-05-07 10:10:38 -05:00
|
|
|
|
2022-07-21 08:36:05 -05:00
|
|
|
// turn uniques into a vector and sort it by the time it was last started
|
|
|
|
let mut uniques: Vec<_> = uniques.into_values().collect();
|
2022-07-21 08:54:43 -05:00
|
|
|
uniques.sort_unstable_by_key(|e| Reverse(e.last_start));
|
2022-05-07 10:10:38 -05:00
|
|
|
|
|
|
|
writeln!(streams.out, "Latest entries of sheet '{current_sheet}':\n")?;
|
|
|
|
|
|
|
|
let formatter = timeago::Formatter::new();
|
|
|
|
|
2022-07-21 08:36:05 -05:00
|
|
|
// Create a table for nicer output
|
|
|
|
let mut table = Tabulate::with_columns(vec![
|
|
|
|
Col::new().min_width(3).and_alignment(Align::Right), // option number
|
|
|
|
Col::new(), // note
|
|
|
|
Col::new().and_alignment(Align::Right), // acumulated time
|
|
|
|
Col::new().min_width(13).and_alignment(Align::Right), // last started
|
|
|
|
]);
|
|
|
|
|
|
|
|
table.feed(vec!["#", "Note", "Total time", "Last started"]);
|
|
|
|
table.separator(' ');
|
|
|
|
|
2022-08-04 05:51:17 -05:00
|
|
|
for (i, entry) in uniques.iter().take(facts.config.interactive_entries).enumerate().rev() {
|
2022-05-07 10:10:38 -05:00
|
|
|
let i = i + 1;
|
2022-07-21 08:36:05 -05:00
|
|
|
let ago = formatter.convert_chrono(entry.last_start, facts.now);
|
|
|
|
|
|
|
|
table.feed(vec![
|
|
|
|
i.to_string(),
|
|
|
|
entry.note.clone(),
|
|
|
|
format_duration(entry.accumulated_time),
|
|
|
|
ago,
|
|
|
|
]);
|
2022-05-07 10:10:38 -05:00
|
|
|
|
|
|
|
}
|
2022-07-21 08:36:05 -05:00
|
|
|
write!(streams.out, "{}", table.print(false))?;
|
2022-05-07 10:10:38 -05:00
|
|
|
|
|
|
|
writeln!(streams.out, "\nenter number or q to cancel")?;
|
|
|
|
|
|
|
|
loop {
|
|
|
|
write!(streams.out, ">> ")?;
|
|
|
|
streams.out.flush()?;
|
|
|
|
|
|
|
|
let choice = to_choice(read_line(&mut streams.r#in)?);
|
|
|
|
|
|
|
|
match choice {
|
2022-07-21 08:36:05 -05:00
|
|
|
Choice::Number(i) => if let Some(e) = uniques.get(i - 1) {
|
|
|
|
return Ok(Some(e.note.clone()));
|
2022-05-07 10:10:38 -05:00
|
|
|
} else {
|
|
|
|
writeln!(streams.out, "Not an option")?;
|
|
|
|
}
|
|
|
|
Choice::Quit => return Ok(None),
|
|
|
|
Choice::CtrlD => {
|
|
|
|
writeln!(streams.out)?;
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
Choice::Whatever => writeln!(streams.out, "Not an option")?,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2022-05-07 22:37:05 -05:00
|
|
|
|
|
|
|
pub fn confirm_deletion<D, I, O, E>(streams: &mut Streams<D, I, O, E>, entry: Entry, now: DateTime<Utc>) -> Result<()>
|
|
|
|
where
|
|
|
|
D: Database,
|
|
|
|
I: BufRead,
|
|
|
|
O: Write,
|
|
|
|
E: Write,
|
|
|
|
{
|
|
|
|
let id = entry.id;
|
|
|
|
let note = entry.note.unwrap_or_else(|| "-empty note-".into());
|
|
|
|
let formatter = {
|
|
|
|
let mut formatter = timeago::Formatter::new();
|
|
|
|
formatter.ago("");
|
|
|
|
formatter
|
|
|
|
};
|
|
|
|
let duration = if let Some(end) = entry.end {
|
|
|
|
let span = formatter.convert_chrono(entry.start, end);
|
|
|
|
format!("finished with a timespan of {span}")
|
|
|
|
} else {
|
|
|
|
let span = formatter.convert_chrono(entry.start, now);
|
|
|
|
format!("unfinished and running for {span}")
|
|
|
|
};
|
|
|
|
|
|
|
|
if ask(streams, &format!("\
|
|
|
|
are you sure you want to delete entry {id} with note
|
|
|
|
|
|
|
|
\"{note}\"
|
|
|
|
|
2022-08-26 11:33:56 -05:00
|
|
|
{duration}?"))? {
|
2022-05-07 22:37:05 -05:00
|
|
|
streams.db.delete_entry_by_id(entry.id)?;
|
|
|
|
writeln!(streams.out, "Gone")?;
|
|
|
|
} else {
|
|
|
|
writeln!(streams.out, "Don't worry, it's still there")?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2022-07-21 08:36:05 -05:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use chrono::Duration;
|
|
|
|
use pretty_assertions::assert_str_eq;
|
|
|
|
|
2022-07-21 08:54:43 -05:00
|
|
|
use crate::config::Config;
|
|
|
|
|
2022-07-21 08:36:05 -05:00
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn interactive_choice_of_tasks() {
|
|
|
|
let mut streams = Streams::fake(b"1\n");
|
|
|
|
let facts = Facts::new();
|
|
|
|
let one_hour_ago = facts.now - Duration::hours(1);
|
|
|
|
let two_hours_ago = facts.now - Duration::hours(2);
|
|
|
|
|
|
|
|
// insert some entries to pick from
|
|
|
|
streams.db.entry_insert(two_hours_ago, Some(one_hour_ago), Some("first task".into()), "default").unwrap();
|
|
|
|
streams.db.entry_insert(one_hour_ago, Some(facts.now), Some("second task".into()), "default").unwrap();
|
|
|
|
|
|
|
|
// call the command interactively
|
2022-08-04 05:51:17 -05:00
|
|
|
assert_eq!(note_from_last_entries(&mut streams, &facts, "default").unwrap().unwrap(), "second task");
|
2022-07-21 08:36:05 -05:00
|
|
|
|
|
|
|
// check the output
|
|
|
|
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Latest entries of sheet 'default':
|
|
|
|
|
|
|
|
# Note Total time Last started
|
|
|
|
|
2022-07-21 08:54:43 -05:00
|
|
|
2 first task 1:00:00 2 hours ago
|
2022-08-04 05:51:17 -05:00
|
|
|
1 second task 1:00:00 1 hour ago
|
2022-07-21 08:54:43 -05:00
|
|
|
|
|
|
|
enter number or q to cancel
|
|
|
|
>> ");
|
|
|
|
assert_str_eq!(&String::from_utf8_lossy(&streams.err), "");
|
|
|
|
}
|
|
|
|
|
|
|
|
/// only the most recently started N items (from settings) are shown and
|
|
|
|
/// they are ordered by start date descending
|
|
|
|
#[test]
|
|
|
|
fn list_is_limited_to_n() {
|
2022-07-21 09:04:25 -05:00
|
|
|
let config = Config {
|
|
|
|
interactive_entries: 4,
|
|
|
|
..Default::default()
|
|
|
|
};
|
2022-07-21 08:54:43 -05:00
|
|
|
let mut streams = Streams::fake(b"1\n");
|
|
|
|
let facts = Facts::new().with_config(config);
|
|
|
|
|
|
|
|
// insert some entries to pick from
|
|
|
|
streams.db.entry_insert(facts.now - Duration::minutes(9), Some(facts.now - Duration::minutes(8)), Some("task 1".into()), "default").unwrap();
|
|
|
|
streams.db.entry_insert(facts.now - Duration::minutes(8), Some(facts.now - Duration::minutes(7)), Some("task 2".into()), "default").unwrap();
|
|
|
|
streams.db.entry_insert(facts.now - Duration::minutes(7), Some(facts.now - Duration::minutes(6)), Some("task 3".into()), "default").unwrap();
|
|
|
|
streams.db.entry_insert(facts.now - Duration::minutes(6), Some(facts.now - Duration::minutes(5)), Some("task 4".into()), "default").unwrap();
|
|
|
|
streams.db.entry_insert(facts.now - Duration::minutes(5), Some(facts.now - Duration::minutes(4)), Some("task 5".into()), "default").unwrap();
|
|
|
|
streams.db.entry_insert(facts.now - Duration::minutes(4), Some(facts.now - Duration::minutes(3)), Some("task 6".into()), "default").unwrap();
|
|
|
|
|
|
|
|
// call the command interactively
|
2022-08-04 05:51:17 -05:00
|
|
|
assert_eq!(note_from_last_entries(&mut streams, &facts, "default").unwrap().unwrap(), "task 6");
|
2022-07-21 08:54:43 -05:00
|
|
|
|
|
|
|
// check the output
|
|
|
|
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Latest entries of sheet 'default':
|
|
|
|
|
|
|
|
# Note Total time Last started
|
|
|
|
|
|
|
|
4 task 3 0:01:00 7 minutes ago
|
2022-08-04 05:51:17 -05:00
|
|
|
3 task 4 0:01:00 6 minutes ago
|
|
|
|
2 task 5 0:01:00 5 minutes ago
|
|
|
|
1 task 6 0:01:00 4 minutes ago
|
2022-07-21 08:36:05 -05:00
|
|
|
|
|
|
|
enter number or q to cancel
|
|
|
|
>> ");
|
|
|
|
assert_str_eq!(&String::from_utf8_lossy(&streams.err), "");
|
|
|
|
}
|
|
|
|
}
|