tabulate interactive's output

This commit is contained in:
Abraham Toriz 2022-07-21 21:36:05 +08:00
parent 8b64b161f2
commit 26af37bff2
No known key found for this signature in database
GPG Key ID: D5B4A746DB5DD42A
4 changed files with 107 additions and 52 deletions

View File

@ -171,27 +171,4 @@ No entry to resume in the sheet 'default'. Perhaps start a new one?
Hint: use t in
");
}
#[test]
fn resume_interactive() {
let args = Args {
entry: SelectedEntry::Interactive,
at: None,
};
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("fake note".into()), "default").unwrap();
// call the command interactively
ResumeCommand::handle(args, &mut streams, &facts).unwrap();
// check the output
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), "");
}
}

View File

@ -59,6 +59,7 @@ pub trait Database {
// -------------
// Entry queries
// -------------
/// Return entries from a sheet ordered by the start date ascending
fn entries_by_sheet(&self, sheet: &str, start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> Result<Vec<Entry>> {
match (start, end) {
(Some(start), Some(end)) => {

View File

@ -46,20 +46,20 @@ pub fn print_formatted<W: Write>(entries: Vec<Entry>, out: &mut W, facts: &Facts
// A vector of lines to be printed, with all the components
let mut tabs = Tabulate::with_columns(vec![
Col::min_width(3).and_alignment(Right),
Col::min_width(18).and_alignment(Left),
Col::min_width(21).and_alignment(Left),
Col::min_width(8).and_alignment(Right),
Col::min_width(5).max_width(44).and_alignment(Left),
Col::new().min_width(3).and_alignment(Right),
Col::new().min_width(18).and_alignment(Left),
Col::new().min_width(21).and_alignment(Left),
Col::new().min_width(8).and_alignment(Right),
Col::new().min_width(5).max_width(44).and_alignment(Left),
]);
if ids {
tabs.feed(vec![
"ID".into(), "Day".into(), "Start End".into(), "Duration".into(), "Notes".into(),
"ID", "Day", "Start End", "Duration", "Notes",
]);
} else {
tabs.feed(vec![
"".into(), "Day".into(), "Start End".into(), "Duration".into(), "Notes".into(),
"", "Day", "Start End", "Duration", "Notes",
]);
}

View File

@ -1,13 +1,15 @@
use std::io::{self, BufRead, Write};
use std::collections::{HashMap, hash_map};
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use chrono::{DateTime, Utc, Duration};
use crate::io::Streams;
use crate::database::Database;
use crate::error::Result;
use crate::commands::Facts;
use crate::models::Entry;
use crate::tabulate::{Tabulate, Col, Align};
use crate::formatters::text::format_duration;
fn read_line<I: BufRead>(mut r#in: I) -> io::Result<String> {
let mut pre_n = String::new();
@ -64,32 +66,72 @@ where
let entries = streams.db.entries_by_sheet(current_sheet, None, None)?;
let mut uniques = HashMap::new();
entries
.into_iter().rev()
.filter_map(|e| e.note.map(|n| (n, e.start)))
.map(|(n, s)| if let hash_map::Entry::Vacant(e) = uniques.entry(n) {
e.insert(s);
true
} else {
false
})
.filter(|&i| i)
.take(facts.config.interactive_entries)
.count();
struct GroupedEntry {
note: String,
last_start: DateTime<Utc>,
accumulated_time: Duration,
}
let mut uniques: Vec<_> = uniques.into_iter().collect();
uniques.sort_unstable_by_key(|(_n, s)| *s);
// 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;
}
// turn uniques into a vector and sort it by the time it was last started
let mut uniques: Vec<_> = uniques.into_values().collect();
uniques.sort_unstable_by_key(|e| e.last_start);
writeln!(streams.out, "Latest entries of sheet '{current_sheet}':\n")?;
let formatter = timeago::Formatter::new();
for (i, (note, time)) in uniques.iter().enumerate() {
let i = i + 1;
let ago = formatter.convert_chrono(*time, facts.now);
// 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(' ');
for (i, entry) in uniques.iter().enumerate() {
let i = i + 1;
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,
]);
writeln!(streams.out, " {i}) {note} ({ago})")?;
}
write!(streams.out, "{}", table.print(false))?;
writeln!(streams.out, "\nenter number or q to cancel")?;
@ -100,8 +142,8 @@ where
let choice = to_choice(read_line(&mut streams.r#in)?);
match choice {
Choice::Number(i) => if let Some((n, _s)) = uniques.get(i - 1) {
return Ok(Some(n.clone()));
Choice::Number(i) => if let Some(e) = uniques.get(i - 1) {
return Ok(Some(e.note.clone()));
} else {
writeln!(streams.out, "Not an option")?;
}
@ -151,3 +193,38 @@ are you sure you want to delete entry {id} with note
Ok(())
}
#[cfg(test)]
mod tests {
use chrono::Duration;
use pretty_assertions::assert_str_eq;
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
note_from_last_entries(&mut streams, &facts, "default").unwrap();
// check the output
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Latest entries of sheet 'default':
# Note Total time Last started
1 first task 1:00:00 2 hours ago
2 second task 1:00:00 1 hour ago
enter number or q to cancel
>> ");
assert_str_eq!(&String::from_utf8_lossy(&streams.err), "");
}
}