tabulate interactive's output
This commit is contained in:
parent
8b64b161f2
commit
26af37bff2
|
@ -171,27 +171,4 @@ No entry to resume in the sheet 'default'. Perhaps start a new one?
|
||||||
Hint: use t in
|
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), "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ pub trait Database {
|
||||||
// -------------
|
// -------------
|
||||||
// Entry queries
|
// 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>> {
|
fn entries_by_sheet(&self, sheet: &str, start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> Result<Vec<Entry>> {
|
||||||
match (start, end) {
|
match (start, end) {
|
||||||
(Some(start), Some(end)) => {
|
(Some(start), Some(end)) => {
|
||||||
|
|
|
@ -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
|
// A vector of lines to be printed, with all the components
|
||||||
let mut tabs = Tabulate::with_columns(vec![
|
let mut tabs = Tabulate::with_columns(vec![
|
||||||
Col::min_width(3).and_alignment(Right),
|
Col::new().min_width(3).and_alignment(Right),
|
||||||
Col::min_width(18).and_alignment(Left),
|
Col::new().min_width(18).and_alignment(Left),
|
||||||
Col::min_width(21).and_alignment(Left),
|
Col::new().min_width(21).and_alignment(Left),
|
||||||
Col::min_width(8).and_alignment(Right),
|
Col::new().min_width(8).and_alignment(Right),
|
||||||
Col::min_width(5).max_width(44).and_alignment(Left),
|
Col::new().min_width(5).max_width(44).and_alignment(Left),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ids {
|
if ids {
|
||||||
tabs.feed(vec![
|
tabs.feed(vec![
|
||||||
"ID".into(), "Day".into(), "Start End".into(), "Duration".into(), "Notes".into(),
|
"ID", "Day", "Start End", "Duration", "Notes",
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
tabs.feed(vec![
|
tabs.feed(vec![
|
||||||
"".into(), "Day".into(), "Start End".into(), "Duration".into(), "Notes".into(),
|
"", "Day", "Start End", "Duration", "Notes",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
use std::io::{self, BufRead, Write};
|
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::io::Streams;
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::commands::Facts;
|
use crate::commands::Facts;
|
||||||
use crate::models::Entry;
|
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> {
|
fn read_line<I: BufRead>(mut r#in: I) -> io::Result<String> {
|
||||||
let mut pre_n = String::new();
|
let mut pre_n = String::new();
|
||||||
|
@ -64,32 +66,72 @@ where
|
||||||
let entries = streams.db.entries_by_sheet(current_sheet, None, None)?;
|
let entries = streams.db.entries_by_sheet(current_sheet, None, None)?;
|
||||||
let mut uniques = HashMap::new();
|
let mut uniques = HashMap::new();
|
||||||
|
|
||||||
entries
|
struct GroupedEntry {
|
||||||
.into_iter().rev()
|
note: String,
|
||||||
.filter_map(|e| e.note.map(|n| (n, e.start)))
|
last_start: DateTime<Utc>,
|
||||||
.map(|(n, s)| if let hash_map::Entry::Vacant(e) = uniques.entry(n) {
|
accumulated_time: Duration,
|
||||||
e.insert(s);
|
}
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.filter(|&i| i)
|
|
||||||
.take(facts.config.interactive_entries)
|
|
||||||
.count();
|
|
||||||
|
|
||||||
let mut uniques: Vec<_> = uniques.into_iter().collect();
|
// From all possible entries belonging to this sheet keep only those with a
|
||||||
uniques.sort_unstable_by_key(|(_n, s)| *s);
|
// 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")?;
|
writeln!(streams.out, "Latest entries of sheet '{current_sheet}':\n")?;
|
||||||
|
|
||||||
let formatter = timeago::Formatter::new();
|
let formatter = timeago::Formatter::new();
|
||||||
|
|
||||||
for (i, (note, time)) in uniques.iter().enumerate() {
|
// Create a table for nicer output
|
||||||
let i = i + 1;
|
let mut table = Tabulate::with_columns(vec![
|
||||||
let ago = formatter.convert_chrono(*time, facts.now);
|
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")?;
|
writeln!(streams.out, "\nenter number or q to cancel")?;
|
||||||
|
|
||||||
|
@ -100,8 +142,8 @@ where
|
||||||
let choice = to_choice(read_line(&mut streams.r#in)?);
|
let choice = to_choice(read_line(&mut streams.r#in)?);
|
||||||
|
|
||||||
match choice {
|
match choice {
|
||||||
Choice::Number(i) => if let Some((n, _s)) = uniques.get(i - 1) {
|
Choice::Number(i) => if let Some(e) = uniques.get(i - 1) {
|
||||||
return Ok(Some(n.clone()));
|
return Ok(Some(e.note.clone()));
|
||||||
} else {
|
} else {
|
||||||
writeln!(streams.out, "Not an option")?;
|
writeln!(streams.out, "Not an option")?;
|
||||||
}
|
}
|
||||||
|
@ -151,3 +193,38 @@ are you sure you want to delete entry {id} with note
|
||||||
|
|
||||||
Ok(())
|
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), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue