diff --git a/Cargo.lock b/Cargo.lock index fbf1ae4..1d15cc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,6 +623,7 @@ name = "tiempo" version = "1.0.4" dependencies = [ "ansi_term 0.12.1", + "atty", "chrono", "clap", "csv", diff --git a/Cargo.toml b/Cargo.toml index c0dabdf..1fcbbd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ lazy_static = "1.4" tempfile = "3" serde_json = "1.0" hostname = "0.3" +atty = "0.2" [dependencies.chrono] version = "0.4" diff --git a/src/commands.rs b/src/commands.rs index 744c6c7..a58569a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,5 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; use chrono::{DateTime, Utc}; @@ -7,6 +7,8 @@ use chrono::{DateTime, Utc}; use crate::error::Result; use crate::database::Database; use crate::config::Config; +use crate::io::Streams; +use crate::env::Env; pub mod r#in; pub mod display; @@ -25,8 +27,44 @@ pub mod edit; pub mod archive; pub mod configure; +pub struct Facts { + pub now: DateTime, + pub config: Config, + pub env: Env, +} + +impl Facts { + pub fn new() -> Facts { + Facts { + now: Utc::now(), + config: Default::default(), + env: Default::default(), + } + } + + pub fn with_config(self, config: Config) -> Facts { + Facts { + config, + ..self + } + } + + pub fn with_now(self, now: DateTime) -> Facts { + Facts { + now, + ..self + } + } +} + +impl Default for Facts { + fn default() -> Facts { + Facts::new() + } +} + pub trait Command<'a> { type Args: TryFrom<&'a ArgMatches<'a>>; - fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()>; + fn handle(args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()>; } diff --git a/src/commands/archive.rs b/src/commands/archive.rs index 9d6c876..f3cffef 100644 --- a/src/commands/archive.rs +++ b/src/commands/archive.rs @@ -1,5 +1,5 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; use chrono::{DateTime, Utc}; @@ -7,13 +7,13 @@ use regex::Regex; use crate::database::Database; use crate::error::{Error, Result}; -use crate::commands::Command; -use crate::config::Config; +use crate::commands::{Command, Facts}; use crate::timeparse::parse_time; use crate::old::{entries_or_warning, time_or_warning}; use crate::formatters::text; use crate::regex::parse_regex; use crate::interactive::ask; +use crate::io::Streams; #[derive(Default)] pub struct Args { @@ -43,19 +43,20 @@ pub struct ArchiveCommand {} impl<'a> Command<'a> for ArchiveCommand { type Args = Args; - fn handle(args: Args, db: &mut D, out: &mut O, _err: &mut E, _config: &Config, now: DateTime) -> Result<()> + fn handle(args: Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { let mut entries = { - let start = args.start.map(|s| time_or_warning(s, db)).transpose()?.map(|s| s.0); - let end = args.end.map(|e| time_or_warning(e, db)).transpose()?.map(|e| e.0); - let current_sheet = db.current_sheet()?.unwrap_or_else(|| "default".into()); + let start = args.start.map(|s| time_or_warning(s, &streams.db)).transpose()?.map(|s| s.0); + let end = args.end.map(|e| time_or_warning(e, &streams.db)).transpose()?.map(|e| e.0); + let current_sheet = streams.db.current_sheet()?.unwrap_or_else(|| "default".into()); let sheet = args.sheet.unwrap_or(current_sheet); - db.entries_by_sheet(&sheet, start, end)? + streams.db.entries_by_sheet(&sheet, start, end)? }; if let Some(re) = args.grep { @@ -63,20 +64,20 @@ impl<'a> Command<'a> for ArchiveCommand { } if args.fake { - let (entries, _) = entries_or_warning(entries, db)?; + let (entries, _) = entries_or_warning(entries, &streams.db)?; text::print_formatted( entries, - out, - now, + &mut streams.out, + facts.now, true, )?; - } else if ask(out, &format!("Archive {} entries?", entries.len()))? { + } else if ask(streams, &format!("Archive {} entries?", entries.len()))? { for entry in entries { - db.entry_update(entry.id, entry.start, entry.end, entry.note, &format!("_{}", entry.sheet))?; + streams.db.entry_update(entry.id, entry.start, entry.end, entry.note, &format!("_{}", entry.sheet))?; } } else { - writeln!(out, "Ok, they're still there")?; + writeln!(streams.out, "Ok, they're still there")?; } Ok(()) diff --git a/src/commands/configure.rs b/src/commands/configure.rs index 2bc395b..c7f1b0d 100644 --- a/src/commands/configure.rs +++ b/src/commands/configure.rs @@ -1,15 +1,15 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use std::path::PathBuf; use clap::ArgMatches; -use chrono::{DateTime, Utc}; use crate::database::Database; use crate::error::{Error, Result}; -use crate::commands::Command; -use crate::config::{Config, WeekDay}; +use crate::commands::{Command, Facts}; +use crate::config::WeekDay; use crate::formatters::Formatter; +use crate::io::Streams; #[derive(Default)] pub struct Args { @@ -85,22 +85,23 @@ pub struct ConfigureCommand {} impl<'a> Command<'a> for ConfigureCommand { type Args = Args; - fn handle(args: Args, _db: &mut D, out: &mut O, _err: &mut E, config: &Config, _now: DateTime) -> Result<()> + fn handle(args: Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { if args.none_given() { - if let Some(path) = config.path.as_deref() { - writeln!(out, "{}", path.display())?; + if let Some(path) = facts.config.path.as_deref() { + writeln!(streams.out, "{}", path.display())?; } else { - writeln!(out, "Config file is in memory")?; + writeln!(streams.out, "Config file is in memory")?; } Ok(()) } else { - let mut new_config = config.clone(); + let mut new_config = facts.config.clone(); if let Some(path) = args.database_file { new_config.database_file = path; @@ -150,14 +151,14 @@ impl<'a> Command<'a> for ConfigureCommand { new_config.week_start = val; } - if let Some(path) = config.path.as_deref() { + if let Some(path) = facts.config.path.as_deref() { let output = new_config.write(path)?; - writeln!(out, "Your new config:\n")?; + writeln!(streams.out, "Your new config:\n")?; - out.write_all(output.as_bytes())?; + streams.out.write_all(output.as_bytes())?; } else { - writeln!(out, "Your config file is in memory and cannot be written to")?; + writeln!(streams.out, "Your config file is in memory and cannot be written to")?; } Ok(()) diff --git a/src/commands/display.rs b/src/commands/display.rs index 7b1320f..c223951 100644 --- a/src/commands/display.rs +++ b/src/commands/display.rs @@ -1,5 +1,5 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use std::str::FromStr; use clap::ArgMatches; @@ -9,12 +9,12 @@ use regex::Regex; use crate::error::{Result, Error}; use crate::database::Database; use crate::formatters::Formatter; -use crate::config::Config; use crate::timeparse::parse_time; use crate::regex::parse_regex; use crate::old::{entries_or_warning, time_or_warning, warn_if_needed}; +use crate::io::Streams; -use super::Command; +use super::{Command, Facts}; // ---------------------------------------------------------------- // Things that are general to all commands that display in some way @@ -39,28 +39,29 @@ impl FromStr for Sheet { } #[allow(clippy::too_many_arguments)] -pub fn entries_for_display( +pub fn entries_for_display( start: Option>, end: Option>, - sheet: Option, db: &mut D, out: &mut O, err: &mut E, + sheet: Option, streams: &mut Streams, format: Formatter, ids: bool, grep: Option, - now: DateTime, + facts: &Facts, ) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let start = start.map(|s| time_or_warning(s, db)).transpose()?.map(|s| s.0); - let end = end.map(|e| time_or_warning(e, db)).transpose()?.map(|e| e.0); + let start = start.map(|s| time_or_warning(s, &streams.db)).transpose()?.map(|s| s.0); + let end = end.map(|e| time_or_warning(e, &streams.db)).transpose()?.map(|e| e.0); let mut entries = match sheet { - Some(Sheet::All) => db.entries_all_visible(start, end)?, - Some(Sheet::Full) => db.entries_full(start, end)?, - Some(Sheet::Sheet(name)) => db.entries_by_sheet(&name, start, end)?, + Some(Sheet::All) => streams.db.entries_all_visible(start, end)?, + Some(Sheet::Full) => streams.db.entries_full(start, end)?, + Some(Sheet::Sheet(name)) => streams.db.entries_by_sheet(&name, start, end)?, None => { - let current_sheet = db.current_sheet()?.unwrap_or_else(|| "default".into()); + let current_sheet = streams.db.current_sheet()?.unwrap_or_else(|| "default".into()); - db.entries_by_sheet(¤t_sheet, start, end)? + streams.db.entries_by_sheet(¤t_sheet, start, end)? } }; @@ -68,16 +69,16 @@ where entries.retain(|e| re.is_match(&e.note.clone().unwrap_or_else(String::new))); } - let (entries, needs_warning) = entries_or_warning(entries, db)?; + let (entries, needs_warning) = entries_or_warning(entries, &streams.db)?; format.print_formatted( entries, - out, - now, + &mut streams.out, + facts.now, ids, )?; - warn_if_needed(err, needs_warning)?; + warn_if_needed(&mut streams.err, needs_warning, &facts.env)?; Ok(()) } @@ -116,9 +117,10 @@ pub struct DisplayCommand { } impl<'a> Command<'a> for DisplayCommand { type Args = Args; - fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { @@ -126,13 +128,11 @@ impl<'a> Command<'a> for DisplayCommand { args.start, args.end, args.sheet, - db, - out, - err, - args.format.unwrap_or_else(|| config.default_formatter.clone()), + streams, + args.format.unwrap_or_else(|| facts.config.default_formatter.clone()), args.ids, args.grep, - now + facts ) } } @@ -140,26 +140,27 @@ impl<'a> Command<'a> for DisplayCommand { #[cfg(test)] mod tests { use chrono::TimeZone; - use ansi_term::Color::Yellow; use pretty_assertions::assert_eq; use crate::database::SqliteDatabase; use crate::test_utils::Ps; + use crate::config::Config; use super::*; #[test] fn display_as_local_time_if_previous_version() { std::env::set_var("TZ", "CST+6"); + let args = Default::default(); - let mut db = SqliteDatabase::from_path("assets/test_old_db.db").unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); + let mut streams = Streams::fake(b"").with_db( + SqliteDatabase::from_path("assets/test_old_db.db").unwrap() + ); + let facts = Facts::new(); - DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); + DisplayCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: default Day Start End Duration Notes Tue Jun 29, 2021 06:26:49 - 07:26:52 1:00:03 lets do some rust 1:00:03 @@ -168,8 +169,8 @@ mod tests { ")); assert_eq!( - String::from_utf8_lossy(&err), - format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")), + String::from_utf8_lossy(&streams.err), + "[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n" ); } @@ -180,47 +181,40 @@ mod tests { start: Some(Utc.ymd(2021, 6, 30).and_hms(10, 5, 0)), ..Default::default() }; - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); + DisplayCommand::handle(args, &mut streams, &facts).unwrap(); - DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("start,end,note,sheet + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("start,end,note,sheet 2021-06-30T10:10:00.000000Z,,hola,default ")); assert_eq!( - String::from_utf8_lossy(&err), + String::from_utf8_lossy(&streams.err), String::new(), ); } #[test] fn filter_by_match() { - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("adios".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("adios".into()), "default".into()).unwrap(); + entries_for_display(None, None, None, &mut streams, Formatter::Csv, true, Some("io".parse().unwrap()), &facts).unwrap(); - entries_for_display(None, None, None, &mut db, &mut out, &mut err, Formatter::Csv, true, Some("io".parse().unwrap()), Utc::now()).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("id,start,end,note,sheet + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("id,start,end,note,sheet 2,2021-06-30T10:10:00.000000Z,,adios,default ")); assert_eq!( - String::from_utf8_lossy(&err), + String::from_utf8_lossy(&streams.err), String::new(), ); } @@ -231,21 +225,17 @@ mod tests { sheet: Some(Sheet::All), ..Default::default() }; - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); std::env::set_var("TZ", "CST+6"); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "sheet2".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "sheet2".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1".into()).unwrap(); + DisplayCommand::handle(args, &mut streams, &facts).unwrap(); - DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: sheet1 + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: sheet1 Day Start End Duration Notes Wed Jun 30, 2021 04:00:00 - 05:00:00 1:00:00 06:00:00 - 07:00:00 1:00:00 @@ -270,21 +260,17 @@ Timesheet: sheet2 sheet: Some(Sheet::Full), ..Default::default() }; - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); std::env::set_var("TZ", "CST+6"); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "_sheet2".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "_sheet2".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1".into()).unwrap(); + DisplayCommand::handle(args, &mut streams, &facts).unwrap(); - DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("\ + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("\ Timesheet: _sheet2 Day Start End Duration Notes Wed Jun 30, 2021 05:00:00 - 06:00:00 1:00:00 @@ -314,24 +300,24 @@ Timesheet: sheet1 end: Some(Utc.ymd(2021, 6, 29).and_hms(13, 0, 0)), ..Default::default() }; - let mut db = SqliteDatabase::from_path("assets/test_old_db.db").unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); + let mut streams = Streams::fake(b"").with_db( + SqliteDatabase::from_path("assets/test_old_db.db").unwrap() + ); + let facts = Facts::new(); // item in database: // start: 2021-06-29 06:26:49.580565 // end: 2021-06-29 07:26:52.816747 - DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); + DisplayCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("start,end,note,sheet + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("start,end,note,sheet 2021-06-29T12:26:49.580565Z,2021-06-29T13:26:52.816747Z,lets do some rust,default ")); assert_eq!( - String::from_utf8_lossy(&err), - format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")), + String::from_utf8_lossy(&streams.err), + "[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n" ); } @@ -340,22 +326,18 @@ Timesheet: sheet1 std::env::set_var("TZ", "CST+6"); let args = Default::default(); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Config { + let mut streams = Streams::fake(b""); + let facts = Facts::new().with_config(Config { default_formatter: Formatter::Ids, ..Default::default() - }; + }); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); + DisplayCommand::handle(args, &mut streams, &facts).unwrap(); - DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); - - assert_eq!(&String::from_utf8_lossy(&out), "1 2\n"); - assert_eq!(String::from_utf8_lossy(&err), ""); + assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n"); + assert_eq!(String::from_utf8_lossy(&streams.err), ""); } } diff --git a/src/commands/edit.rs b/src/commands/edit.rs index ede696d..8994639 100644 --- a/src/commands/edit.rs +++ b/src/commands/edit.rs @@ -1,17 +1,17 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; use chrono::{DateTime, Utc}; use crate::database::{Database, DBVersion}; use crate::error::{Error, Result}; -use crate::commands::Command; -use crate::config::Config; +use crate::commands::{Facts, Command}; use crate::timeparse::parse_time; use crate::old::{entries_or_warning, time_or_warning, warn_if_needed}; use crate::formatters::text; use crate::editor; +use crate::io::Streams; #[derive(Default)] pub struct Args { @@ -52,69 +52,70 @@ pub struct EditCommand {} impl<'a> Command<'a> for EditCommand { type Args = Args; - fn handle(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let current_sheet = db.current_sheet()?.unwrap_or_else(|| "default".to_owned()); - let needs_warning = db.version()? == DBVersion::Timetrap; + let current_sheet = streams.db.current_sheet()?.unwrap_or_else(|| "default".to_owned()); + let needs_warning = streams.db.version()? == DBVersion::Timetrap; let entry = if let Some(id) = args.id { - if let Some(entry) = db.entry_by_id(id)? { + if let Some(entry) = streams.db.entry_by_id(id)? { entry } else { - writeln!(out, "Entry with id \"{}\" does not exist. Perhaps it was deleted", id)?; + writeln!(streams.out, "Entry with id \"{}\" does not exist. Perhaps it was deleted", id)?; return Ok(()); } - } else if let Some(entry) = db.last_entry_of_sheet(¤t_sheet)? { + } else if let Some(entry) = streams.db.last_entry_of_sheet(¤t_sheet)? { entry } else { - writeln!(out, "No entries to edit in sheet \"{}\".", current_sheet)?; + writeln!(streams.out, "No entries to edit in sheet \"{}\".", current_sheet)?; return Ok(()); }; // normalize the entry in case it comes from an old database - let entry = entries_or_warning(vec![entry], db)?.0.into_iter().next().unwrap(); + let entry = entries_or_warning(vec![entry], &streams.db)?.0.into_iter().next().unwrap(); // try really hard to obtain the note let note = if let Some(new_note) = args.note { // either from the command's args if args.append { - Some(entry.note.unwrap_or_else(|| "".to_owned()) + &config.append_notes_delimiter + &new_note) + Some(entry.note.unwrap_or_else(|| "".to_owned()) + &facts.config.append_notes_delimiter + &new_note) } else { Some(new_note) } } else if args.none_given() { // or from the editor if no arguments where given - Some(editor::get_string(config.note_editor.as_deref(), entry.note)?) + Some(editor::get_string(facts.config.note_editor.as_deref(), entry.note)?) } else { // or just use watever was previously there is the user is editing // something else entry.note }; - db.entry_update( + streams.db.entry_update( entry.id, - time_or_warning(args.start.unwrap_or(entry.start), db)?.0, - args.end.or(entry.end).map(|e| time_or_warning(e, db)).transpose()?.map(|o| o.0), + time_or_warning(args.start.unwrap_or(entry.start), &streams.db)?.0, + args.end.or(entry.end).map(|e| time_or_warning(e, &streams.db)).transpose()?.map(|o| o.0), note, &args.r#move.unwrap_or(entry.sheet), )?; - let updated_entry = entries_or_warning(vec![db.entry_by_id(entry.id)?.unwrap()], db)?.0; + let updated_entry = entries_or_warning(vec![streams.db.entry_by_id(entry.id)?.unwrap()], &streams.db)?.0; text::print_formatted( updated_entry, - out, - now, + &mut streams.out, + facts.now, true, )?; - warn_if_needed(err, needs_warning)?; + warn_if_needed(&mut streams.err, needs_warning, &facts.env)?; Ok(()) } @@ -126,55 +127,50 @@ mod tests { use pretty_assertions::assert_eq; use chrono::{Duration, TimeZone}; - use ansi_term::Color::Yellow; use tempfile::NamedTempFile; use crate::database::SqliteDatabase; use crate::test_utils::Ps; + use crate::config::Config; use super::*; #[test] fn edit_last_note() { std::env::set_var("TZ", "CST+6"); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b""); let args = Args { note: Some("new note".into()), ..Default::default() }; let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let an_hour_ago = now - Duration::hours(1); + let facts = Facts::new().with_now(now); - db.init().unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("should be left intact".into()), "sheet1").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("should be left intact".into()), "sheet1").unwrap(); + EditCommand::handle(args, &mut streams, &facts).unwrap(); - EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - let entry2 = db.entry_by_id(2).unwrap().unwrap(); + let entry2 = streams.db.entry_by_id(2).unwrap().unwrap(); assert_eq!(entry2.note, Some("should be left intact".into())); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 13:29:00 - 14:29:00 1:00:00 new note 1:00:00 -------------------------------------------------------------- Total 1:00:00 ")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn edit_with_id() { std::env::set_var("TZ", "CST+6"); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b""); let args = Args { id: Some(2), note: Some("new note".into()), @@ -182,88 +178,79 @@ mod tests { }; let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let an_hour_ago = now - Duration::hours(1); + let facts = Facts::new().with_now(now); - db.init().unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("should be left intact".into()), "sheet1").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("should be left intact".into()), "sheet1").unwrap(); + EditCommand::handle(args, &mut streams, &facts).unwrap(); - EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: sheet1 + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: sheet1 ID Day Start End Duration Notes 2 Tue Aug 03, 2021 13:29:00 - 14:29:00 1:00:00 new note 1:00:00 -------------------------------------------------------------- Total 1:00:00 ")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn edit_start() { std::env::set_var("TZ", "CST+6"); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b""); let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let args = Args { start: Some(now - Duration::minutes(30)), ..Default::default() }; let an_hour_ago = now - Duration::hours(1); + let facts = Facts::new().with_now(now); - db.init().unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); + EditCommand::handle(args, &mut streams, &facts).unwrap(); - EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 13:59:00 - 14:29:00 0:30:00 a note 0:30:00 ------------------------------------------------------------ Total 0:30:00 ")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn edit_end() { std::env::set_var("TZ", "CST+6"); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b""); let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let args = Args { end: Some(now - Duration::minutes(30)), ..Default::default() }; let an_hour_ago = now - Duration::hours(1); + let facts = Facts::new().with_now(now); - db.init().unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); + EditCommand::handle(args, &mut streams, &facts).unwrap(); - EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 13:29:00 - 13:59:00 0:30:00 a note 0:30:00 ------------------------------------------------------------ Total 0:30:00 ")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn edit_append() { std::env::set_var("TZ", "CST+6"); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b""); let args = Args { note: Some("new note".into()), append: true, @@ -271,59 +258,53 @@ mod tests { }; let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let an_hour_ago = now - Duration::hours(1); + let facts = Facts::new().with_now(now); - db.init().unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); + EditCommand::handle(args, &mut streams, &facts).unwrap(); - EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 13:29:00 - 14:29:00 1:00:00 a note new note 1:00:00 --------------------------------------------------------------------- Total 1:00:00 ")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn edit_move() { std::env::set_var("TZ", "CST+6"); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b""); let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let args = Args { r#move: Some("new sheet".to_owned()), ..Default::default() }; let an_hour_ago = now - Duration::hours(1); + let facts = Facts::new().with_now(now); - db.init().unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); + EditCommand::handle(args, &mut streams, &facts).unwrap(); - EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: new sheet + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: new sheet ID Day Start End Duration Notes 1 Tue Aug 03, 2021 13:29:00 - 14:29:00 1:00:00 a note 1:00:00 ------------------------------------------------------------ Total 1:00:00 ")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn non_default_delimiter() { std::env::set_var("TZ", "CST+6"); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b""); let args = Args { note: Some("new note".into()), append: true, @@ -331,25 +312,23 @@ mod tests { }; let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let an_hour_ago = now - Duration::hours(1); - let config = Config { + let facts = Facts::new().with_now(now).with_config(Config { append_notes_delimiter: ";".to_owned(), ..Default::default() - }; + }); - db.init().unwrap(); + streams.db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); - db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); + EditCommand::handle(args, &mut streams, &facts).unwrap(); - EditCommand::handle(args, &mut db, &mut out, &mut err, &config, now).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 13:29:00 - 14:29:00 1:00:00 a note;new note 1:00:00 --------------------------------------------------------------------- Total 1:00:00 ")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } fn copy_db(path: &str) -> NamedTempFile { @@ -365,19 +344,20 @@ mod tests { std::env::set_var("TZ", "CST+6"); let database_file = copy_db("assets/test_old_db.db"); - let mut db = SqliteDatabase::from_path(&database_file).unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); + let mut streams = Streams::fake(b"").with_db( + SqliteDatabase::from_path(&database_file).unwrap() + ); let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let new_end = Utc.ymd(2021, 06, 29).and_hms(14, 26, 52); let args = Args { end: Some(new_end), ..Default::default() }; + let facts = Facts::new().with_now(now); - EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); + EditCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Jun 29, 2021 06:26:49 - 08:26:52 2:00:02 lets do some rust 2:00:02 @@ -385,11 +365,11 @@ mod tests { Total 2:00:02 ")); assert_eq!( - String::from_utf8_lossy(&err), - format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")), + String::from_utf8_lossy(&streams.err), + "[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n" ); - std::mem::drop(db); + std::mem::drop(streams.db); std::mem::drop(database_file); } } diff --git a/src/commands/in.rs b/src/commands/in.rs index 28ecbc1..e3839d9 100644 --- a/src/commands/in.rs +++ b/src/commands/in.rs @@ -1,5 +1,5 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; use chrono::{DateTime, Utc}; @@ -7,10 +7,10 @@ use chrono::{DateTime, Utc}; use crate::database::Database; use crate::error::{Error, Result}; use crate::editor; -use crate::commands::Command; -use crate::config::Config; +use crate::commands::{Command, Facts}; use crate::timeparse::parse_time; use crate::old::{time_or_warning, warn_if_needed}; +use crate::io::Streams; #[derive(Default)] pub struct Args { @@ -34,36 +34,37 @@ pub struct InCommand {} impl<'a> Command<'a> for InCommand { type Args = Args; - fn handle(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let start = args.at.unwrap_or(now); - let sheet = db.current_sheet()?.unwrap_or_else(|| "default".into()); + let start = args.at.unwrap_or(facts.now); + let sheet = streams.db.current_sheet()?.unwrap_or_else(|| "default".into()); - if db.running_entry(&sheet)?.is_some() { - writeln!(out, "Timer is already running for sheet '{}'", sheet)?; + if streams.db.running_entry(&sheet)?.is_some() { + writeln!(streams.out, "Timer is already running for sheet '{}'", sheet)?; return Ok(()); } let note = if let Some(note) = args.note { Some(note.trim().to_owned()) - } else if !config.require_note { + } else if !facts.config.require_note { None } else { - Some(editor::get_string(config.note_editor.as_deref(), None)?) + Some(editor::get_string(facts.config.note_editor.as_deref(), None)?) }; - let (start, needs_warning) = time_or_warning(start, db)?; + let (start, needs_warning) = time_or_warning(start, &streams.db)?; - db.entry_insert(start, None, note, &sheet)?; + streams.db.entry_insert(start, None, note, &sheet)?; - writeln!(out, "Checked into sheet \"{}\".", sheet)?; + writeln!(streams.out, "Checked into sheet \"{}\".", sheet)?; - warn_if_needed(err, needs_warning)?; + warn_if_needed(&mut streams.err, needs_warning, &facts.env)?; Ok(()) } @@ -72,151 +73,135 @@ impl<'a> Command<'a> for InCommand { #[cfg(test)] mod tests { use pretty_assertions::assert_eq; - use ansi_term::Color::Yellow; use chrono::{TimeZone, Local}; use crate::test_utils::Ps; use crate::database::SqliteDatabase; + use crate::config::Config; use super::*; #[test] fn handles_new_entry() { - let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, note: Some("hola".into()), }; - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - d.init().unwrap(); + assert_eq!(streams.db.entries_full(None, None).unwrap().len(), 0); - assert_eq!(d.entries_full(None, None).unwrap().len(), 0); + InCommand::handle(args, &mut streams, &facts).unwrap(); - InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); - - let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); + let e = streams.db.entries_full(None, None).unwrap().into_iter().next().unwrap(); assert_eq!(e.note, Some("hola".into())); - assert_eq!(e.start, now); + assert_eq!(e.start, facts.now); assert_eq!(e.end, None); assert_eq!(e.sheet, "default".to_owned()); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Checked into sheet \"default\".\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Checked into sheet \"default\".\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn test_handles_already_running_entry() { - let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, note: Some("hola".into()), }; - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - d.init().unwrap(); + streams.db.entry_insert(facts.now, None, None, "default".into()).unwrap(); - d.entry_insert(now, None, None, "default".into()).unwrap(); + assert_eq!(streams.db.entries_full(None, None).unwrap().len(), 1); - assert_eq!(d.entries_full(None, None).unwrap().len(), 1); + InCommand::handle(args, &mut streams, &facts).unwrap(); - InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), Utc::now()).unwrap(); - - assert_eq!(d.entries_full(None, None).unwrap().len(), 1); + assert_eq!(streams.db.entries_full(None, None).unwrap().len(), 1); assert_eq!( - Ps(&String::from_utf8_lossy(&out)), + Ps(&String::from_utf8_lossy(&streams.out)), Ps("Timer is already running for sheet 'default'\n") ); } #[test] fn no_note_and_no_mandatory_leaves_none() { - let mut d = SqliteDatabase::from_memory().unwrap(); let args = Default::default(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); - let config = Config { + let mut streams = Streams::fake(b""); + let facts = Facts::new().with_config(Config { require_note: false, ..Default::default() - }; + }); - d.init().unwrap(); + assert_eq!(streams.db.entries_full(None, None).unwrap().len(), 0); - assert_eq!(d.entries_full(None, None).unwrap().len(), 0); + InCommand::handle(args, &mut streams, &facts).unwrap(); - InCommand::handle(args, &mut d, &mut out, &mut err, &config, now).unwrap(); - - let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); + let e = streams.db.entries_full(None, None).unwrap().into_iter().next().unwrap(); assert_eq!(e.note, None); - assert_eq!(e.start, now); + assert_eq!(e.start, facts.now); assert_eq!(e.end, None); assert_eq!(e.sheet, "default".to_owned()); } #[test] fn warns_if_old_database() { - let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, note: Some("hola".into()), }; - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b"").with_db({ + let mut db = SqliteDatabase::from_memory().unwrap(); - d.init_old().unwrap(); + db.init_old().unwrap(); - assert_eq!(d.entries_full(None, None).unwrap().len(), 0); + db + }); + let facts = Facts::new(); - InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); + assert_eq!(streams.db.entries_full(None, None).unwrap().len(), 0); - let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); + InCommand::handle(args, &mut streams, &facts).unwrap(); + + let e = streams.db.entries_full(None, None).unwrap().into_iter().next().unwrap(); assert_eq!(e.note, Some("hola".into())); - assert_eq!(e.start, Utc.from_utc_datetime(&now.with_timezone(&Local).naive_local())); + assert_eq!(e.start, Utc.from_utc_datetime(&facts.now.with_timezone(&Local).naive_local())); assert_eq!(e.end, None); assert_eq!(e.sheet, "default".to_owned()); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Checked into sheet \"default\".\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps(&format!( - "{} You are using the old timetrap format, it is advised that \ - you update your database using t migrate\n", - Yellow.bold().paint("[WARNING]")))); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Checked into sheet \"default\".\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps( + "[WARNING] You are using the old timetrap format, it is advised that \ + you update your database using t migrate\n")); } #[test] fn notes_are_trimmed() { - let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, note: Some("hola\n".into()), }; - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - d.init().unwrap(); + assert_eq!(streams.db.entries_full(None, None).unwrap().len(), 0); - assert_eq!(d.entries_full(None, None).unwrap().len(), 0); + InCommand::handle(args, &mut streams, &facts).unwrap(); - InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); - - let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); + let e = streams.db.entries_full(None, None).unwrap().into_iter().next().unwrap(); assert_eq!(e.note, Some("hola".into())); - assert_eq!(e.start, now); + assert_eq!(e.start, facts.now); assert_eq!(e.end, None); assert_eq!(e.sheet, "default".to_owned()); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Checked into sheet \"default\".\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Checked into sheet \"default\".\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } } diff --git a/src/commands/kill.rs b/src/commands/kill.rs index 6956906..69b8ed3 100644 --- a/src/commands/kill.rs +++ b/src/commands/kill.rs @@ -1,15 +1,14 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; -use chrono::{DateTime, Utc}; use crate::error::{Error, Result}; use crate::database::Database; -use crate::config::Config; use crate::interactive::ask; +use crate::io::Streams; -use super::Command; +use super::{Command, Facts}; #[derive(Debug)] pub enum Args { @@ -34,33 +33,34 @@ pub struct KillCommand; impl<'a> Command<'a> for KillCommand { type Args = Args; - fn handle(args: Args, db: &mut D, out: &mut O, _err: &mut E, _config: &Config, _now: DateTime) -> Result<()> + fn handle(args: Args, streams: &mut Streams, _facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { match args { Args::Id(id) => { - if let Some(entry) = db.entry_by_id(id)? { - if ask(out, &format!("are you sure you want to delete entry {}? ({})", entry.id, entry.note.unwrap_or_else(|| "".into())))? { - db.delete_entry_by_id(id)?; - writeln!(out, "It's dead")?; + if let Some(entry) = streams.db.entry_by_id(id)? { + if ask(streams, &format!("are you sure you want to delete entry {}? ({})", entry.id, entry.note.unwrap_or_else(|| "".into())))? { + streams.db.delete_entry_by_id(id)?; + writeln!(streams.out, "It's dead")?; } else { - writeln!(out, "Don't worry, it's still there")?; + writeln!(streams.out, "Don't worry, it's still there")?; } } else { - writeln!(out, "There's no entry with id {}. Someone found it before we did.", id)?; + writeln!(streams.out, "There's no entry with id {}. Someone found it before we did.", id)?; } }, Args::Sheet(sheet) => { - let n = db.entries_by_sheet(&sheet, None, None)?.len(); + let n = streams.db.entries_by_sheet(&sheet, None, None)?.len(); - if ask(out, &format!("are you sure you want to delete {} entries on sheet \"{}\"?", n, sheet))? { - db.delete_entries_in_sheet(&sheet)?; - writeln!(out, "They're gone")?; + if ask(streams, &format!("are you sure you want to delete {} entries on sheet \"{}\"?", n, sheet))? { + streams.db.delete_entries_in_sheet(&sheet)?; + writeln!(streams.out, "They're gone")?; } else { - writeln!(out, "Don't worry, they're still there")?; + writeln!(streams.out, "Don't worry, they're still there")?; } } } diff --git a/src/commands/list.rs b/src/commands/list.rs index 1ddc78a..482b585 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,20 +1,20 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; -use chrono::{DateTime, Utc, Duration, Local}; +use chrono::{Utc, Duration, Local}; use itertools::Itertools; use ansi_term::Style; use crate::error::{Error, Result}; use crate::database::Database; -use crate::config::Config; use crate::tabulate::{Tabulate, Col, Align::*}; use crate::formatters::text::format_duration; use crate::models::Entry; use crate::old::{entries_or_warning, warn_if_needed}; +use crate::io::Streams; -use super::Command; +use super::{Command, Facts}; #[derive(Default)] pub struct Args { @@ -36,28 +36,29 @@ pub struct ListCommand {} impl<'a> Command<'a> for ListCommand { type Args = Args; - fn handle(args: Args, db: &mut D, out: &mut O, err: &mut E, _config: &Config, now: DateTime) -> Result<()> + fn handle(args: Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let today = now.with_timezone(&Local).date().and_hms(0, 0, 0).with_timezone(&Utc); + let today = facts.now.with_timezone(&Local).date().and_hms(0, 0, 0).with_timezone(&Utc); let entries = if args.all { - db.entries_full(None, None)? + streams.db.entries_full(None, None)? } else { - db.entries_all_visible(None, None)? + streams.db.entries_all_visible(None, None)? }; - let (mut entries, needs_warning) = entries_or_warning(entries, db)?; + let (mut entries, needs_warning) = entries_or_warning(entries, &streams.db)?; - let current = db.current_sheet()?; - let last = db.last_sheet()?; + let current = streams.db.current_sheet()?; + let last = streams.db.last_sheet()?; // introducte two fake entries to make both current and last show up if let Some(ref current) = current { entries.push(Entry { - id: 1, sheet: current.clone(), start: now, end: Some(now), note: None, + id: 1, sheet: current.clone(), start: facts.now, end: Some(facts.now), note: None, }); } @@ -73,12 +74,12 @@ impl<'a> Command<'a> for ListCommand { .into_iter() .map(|(key, group)| { let entries: Vec<_> = group.into_iter().collect(); - let s_running = now - entries.iter().find(|e| e.end.is_none()).map(|e| e.start).unwrap_or(now); + let s_running = facts.now - entries.iter().find(|e| e.end.is_none()).map(|e| e.start).unwrap_or(facts.now); let s_today = entries.iter().filter(|e| e.start > today).fold(Duration::seconds(0), |acc, e| { - acc + (e.end.unwrap_or(now) - e.start) + acc + (e.end.unwrap_or(facts.now) - e.start) }); let s_total = entries.into_iter().fold(Duration::seconds(0), |acc, e| { - acc + (e.end.unwrap_or(now) - e.start) + acc + (e.end.unwrap_or(facts.now) - e.start) }); total_running = total_running + s_running; @@ -139,9 +140,9 @@ impl<'a> Command<'a> for ListCommand { format_duration(total), ]); - out.write_all(tabs.print().as_bytes())?; + streams.out.write_all(tabs.print().as_bytes())?; - warn_if_needed(err, needs_warning)?; + warn_if_needed(&mut streams.err, needs_warning, &facts.env)?; Ok(()) } @@ -151,7 +152,7 @@ impl<'a> Command<'a> for ListCommand { mod tests { use chrono::{Utc, TimeZone}; use pretty_assertions::assert_eq; - use ansi_term::{Color::Yellow, Style}; + use ansi_term::Style; use crate::database::{SqliteDatabase, Database}; use crate::test_utils::Ps; @@ -163,25 +164,23 @@ mod tests { std::env::set_var("TZ", "CST+6"); let args = Default::default(); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); - db.init().unwrap(); - db.set_current_sheet("sheet2").unwrap(); - db.set_last_sheet("sheet4").unwrap(); + let mut streams = Streams::fake(b""); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(1, 0, 0)), None, "_archived".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(10,13, 55)), None, "sheet1".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(7, 39, 18)), None, "sheet3".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(13, 52, 45)), None, "sheet3".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), None, None, "sheet4".into()).unwrap(); + streams.db.set_current_sheet("sheet2").unwrap(); + streams.db.set_last_sheet("sheet4").unwrap(); + + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(1, 0, 0)), None, "_archived".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(10,13, 55)), None, "sheet1".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(7, 39, 18)), None, "sheet3".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(13, 52, 45)), None, "sheet3".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), None, None, "sheet4".into()).unwrap(); let now = Utc.ymd(2021, 1, 1).and_hms(13, 52, 45); + let facts = Facts::new().with_now(now); - ListCommand::handle(args, &mut db, &mut out, &mut err, &config, now).unwrap(); + ListCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps(&format!(" Timesheet Running Today Total Time + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps(&format!(" Timesheet Running Today Total Time sheet1 {0} {0} 10:13:55 * sheet2 {0} {0} 0:00:00 @@ -192,15 +191,15 @@ mod tests { ", Style::new().dimmed().paint(" 0:00:00")))); // now show all the sheets - let mut out = Vec::new(); - let mut err = Vec::new(); + streams.reset_io(); + let args = Args { all: true, }; - ListCommand::handle(args, &mut db, &mut out, &mut err, &config, now).unwrap(); + ListCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps(&format!(" Timesheet Running Today Total Time + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps(&format!(" Timesheet Running Today Total Time _archived {0} {0} 1:00:00 sheet1 {0} {0} 10:13:55 @@ -215,16 +214,16 @@ mod tests { #[test] fn old_database() { let args = Default::default(); - let mut db = SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); + let mut streams = Streams::fake(b"").with_db( + SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap() + ); let now = Local.ymd(2021, 7, 16).and_hms(11, 30, 45); + let facts = Facts::new().with_now(now.with_timezone(&Utc)); - ListCommand::handle(args, &mut db, &mut out, &mut err, &config, now.with_timezone(&Utc)).unwrap(); + ListCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps(" Timesheet Running Today Total Time + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps(" Timesheet Running Today Total Time * default 0:10:24 0:10:26 0:10:26 -------------------------------------------- @@ -232,8 +231,8 @@ mod tests { ")); assert_eq!( - String::from_utf8_lossy(&err), - format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")), + String::from_utf8_lossy(&streams.err), + "[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n" ); } } diff --git a/src/commands/month.rs b/src/commands/month.rs index 37ad4eb..64abb15 100644 --- a/src/commands/month.rs +++ b/src/commands/month.rs @@ -1,5 +1,5 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use std::str::FromStr; use clap::ArgMatches; @@ -9,10 +9,10 @@ use regex::Regex; use crate::error::{Result, Error}; use crate::database::Database; use crate::formatters::Formatter; -use crate::config::Config; use crate::regex::parse_regex; +use crate::io::Streams; -use super::{Command, display::{Sheet, entries_for_display}}; +use super::{Command, Facts, display::{Sheet, entries_for_display}}; /// Given a local datetime, returns the time when the month it belongs started fn beginning_of_month(time: DateTime) -> DateTime { @@ -93,12 +93,14 @@ pub struct MonthCommand { } impl<'a> Command<'a> for MonthCommand { type Args = Args; - fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { + let now = facts.now; let (start, end) = match args.month { MonthSpec::This => (beginning_of_month(now.with_timezone(&Local)), now), MonthSpec::Last => { @@ -129,13 +131,22 @@ impl<'a> Command<'a> for MonthCommand { }, }; - entries_for_display(Some(start), Some(end), args.sheet, db, out, err, args.format.unwrap_or_else(|| config.default_formatter.clone()), args.ids, args.grep, now) + entries_for_display( + Some(start), + Some(end), + args.sheet, + streams, + args.format.unwrap_or_else(|| facts.config.default_formatter.clone()), + args.ids, + args.grep, + facts + ) } } #[cfg(test)] mod tests { - use crate::database::SqliteDatabase; + use crate::config::Config; use super::*; @@ -144,22 +155,19 @@ mod tests { std::env::set_var("TZ", "CST+6"); let args = Default::default(); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Config { + let mut streams = Streams::fake(b""); + let now = Utc.ymd(2021, 6, 30).and_hms(11, 0, 0); + let facts = Facts::new().with_config(Config { default_formatter: Formatter::Ids, ..Default::default() - }; + }).with_now(now); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); + MonthCommand::handle(args, &mut streams, &facts).unwrap(); - MonthCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)).unwrap(); - - assert_eq!(&String::from_utf8_lossy(&out), "1 2\n"); - assert_eq!(String::from_utf8_lossy(&err), ""); + assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n"); + assert_eq!(String::from_utf8_lossy(&streams.err), ""); } } diff --git a/src/commands/now.rs b/src/commands/now.rs index 9eacc18..fcea0e2 100644 --- a/src/commands/now.rs +++ b/src/commands/now.rs @@ -1,17 +1,16 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; -use chrono::{DateTime, Utc}; use crate::error::{Result, Error}; use crate::database::Database; -use crate::config::Config; use crate::old::{entries_or_warning, warn_if_needed}; use crate::tabulate::{Tabulate, Col, Align::*}; use crate::formatters::text::format_duration; +use crate::io::Streams; -use super::Command; +use super::{Command, Facts}; #[derive(Default)] pub struct Args { @@ -30,18 +29,19 @@ pub struct NowCommand { } impl<'a> Command<'a> for NowCommand { type Args = Args; - fn handle(_args: Self::Args, db: &mut D, out: &mut O, err: &mut E, _config: &Config, now: DateTime) -> Result<()> + fn handle(_args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let entries = db.running_entries()?; + let entries = streams.db.running_entries()?; - let (entries, needs_warning) = entries_or_warning(entries, db)?; + let (entries, needs_warning) = entries_or_warning(entries, &streams.db)?; - let current = db.current_sheet()?; - let last = db.last_sheet()?; + let current = streams.db.current_sheet()?; + let last = streams.db.last_sheet()?; let mut tabs = Tabulate::with_columns(vec![ // indicator of current or prev sheet @@ -70,14 +70,14 @@ impl<'a> Command<'a> for NowCommand { "".into() }, entry.sheet, - format_duration(now - entry.start), + format_duration(facts.now - entry.start), entry.note.unwrap_or_else(|| "".into()) ]); } - out.write_all(tabs.print().as_bytes())?; + streams.out.write_all(tabs.print().as_bytes())?; - warn_if_needed(err, needs_warning)?; + warn_if_needed(&mut streams.err, needs_warning, &facts.env)?; Ok(()) } @@ -87,7 +87,6 @@ impl<'a> Command<'a> for NowCommand { mod tests { use chrono::{Utc, TimeZone, Local}; use pretty_assertions::assert_eq; - use ansi_term::Color::Yellow; use crate::database::{SqliteDatabase, Database}; use crate::test_utils::Ps; @@ -98,25 +97,22 @@ mod tests { fn list_sheets() { std::env::set_var("TZ", "CST+6"); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); - db.init().unwrap(); - db.set_current_sheet("sheet2").unwrap(); - db.set_last_sheet("sheet4").unwrap(); - - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(1, 0, 0)), None, "_archived".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(10,13, 55)), None, "sheet1".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(7, 39, 18)), None, "sheet3".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(13, 52, 45)), None, "sheet3".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), None, Some("some".into()), "sheet4".into()).unwrap(); - + let mut streams = Streams::fake(b""); let now = Utc.ymd(2021, 1, 1).and_hms(13, 52, 45); + let facts = Facts::new().with_now(now); - NowCommand::handle(Default::default(), &mut db, &mut out, &mut err, &config, now).unwrap(); + streams.db.set_current_sheet("sheet2").unwrap(); + streams.db.set_last_sheet("sheet4").unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps(" Timesheet Running Activity + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(1, 0, 0)), None, "_archived".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(10,13, 55)), None, "sheet1".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(7, 39, 18)), None, "sheet3".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(13, 52, 45)), None, "sheet3".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), None, Some("some".into()), "sheet4".into()).unwrap(); + + NowCommand::handle(Default::default(), &mut streams, &facts).unwrap(); + + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps(" Timesheet Running Activity - sheet4 1:52:45 some ")); @@ -124,23 +120,23 @@ mod tests { #[test] fn old_database() { - let mut db = SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); + let mut streams = Streams::fake(b"").with_db( + SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap() + ); let now = Local.ymd(2021, 7, 16).and_hms(11, 30, 45); + let facts = Facts::new().with_now(now.with_timezone(&Utc)); - NowCommand::handle(Default::default(), &mut db, &mut out, &mut err, &config, now.with_timezone(&Utc)).unwrap(); + NowCommand::handle(Default::default(), &mut streams, &facts).unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps(" Timesheet Running Activity + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps(" Timesheet Running Activity * default 0:10:24 que ")); assert_eq!( - String::from_utf8_lossy(&err), - format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")), + String::from_utf8_lossy(&streams.err), + "[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n" ); } } diff --git a/src/commands/out.rs b/src/commands/out.rs index c9f2a26..9799538 100644 --- a/src/commands/out.rs +++ b/src/commands/out.rs @@ -1,16 +1,16 @@ -use std::io::Write; +use std::io::{BufRead, Write}; use std::convert::TryFrom; use chrono::{DateTime, Utc}; use clap::ArgMatches; use crate::database::Database; -use crate::config::Config; use crate::error::{Error, Result}; use crate::timeparse::parse_time; use crate::old::{time_or_warning, warn_if_needed}; +use crate::io::Streams; -use super::Command; +use super::{Command, Facts}; #[derive(Default)] pub struct Args { @@ -32,21 +32,27 @@ pub struct OutCommand{} impl<'a> Command<'a> for OutCommand { type Args = Args; - fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, _config: &Config, now: DateTime) -> Result<()> { - let end = args.at.unwrap_or(now); - let sheet = db.current_sheet()?.unwrap_or_else(|| "default".into()); + fn handle(args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()> + where + D: Database, + I: BufRead, + O: Write, + E: Write, + { + let end = args.at.unwrap_or(facts.now); + let sheet = streams.db.current_sheet()?.unwrap_or_else(|| "default".into()); - let (end, needs_warning) = time_or_warning(end, db)?; + let (end, needs_warning) = time_or_warning(end, &streams.db)?; - if let Some(entry) = db.running_entry(&sheet)? { - writeln!(out, "Checked out of sheet \"{}\".", sheet)?; + if let Some(entry) = streams.db.running_entry(&sheet)? { + writeln!(streams.out, "Checked out of sheet \"{}\".", sheet)?; - db.entry_update(entry.id, entry.start, Some(end), entry.note, &entry.sheet)?; + streams.db.entry_update(entry.id, entry.start, Some(end), entry.note, &entry.sheet)?; } else { - writeln!(out, "No running entry on sheet \"{}\".", sheet)?; + writeln!(streams.out, "No running entry on sheet \"{}\".", sheet)?; } - warn_if_needed(err, needs_warning)?; + warn_if_needed(&mut streams.err, needs_warning, &facts.env)?; Ok(()) } @@ -54,77 +60,62 @@ impl<'a> Command<'a> for OutCommand { #[cfg(test)] mod tests { - use ansi_term::Color::Yellow; use pretty_assertions::assert_eq; use chrono::{TimeZone, Local}; use crate::test_utils::Ps; - use crate::database::SqliteDatabase; use super::*; #[test] fn finishes_entry() { - let mut db = SqliteDatabase::from_memory().unwrap(); let args = Default::default(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - db.init().unwrap(); + streams.db.entry_insert(facts.now, None, None, "default").unwrap(); - db.entry_insert(now, None, None, "default").unwrap(); + OutCommand::handle(args, &mut streams, &facts).unwrap(); - OutCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); + let e = streams.db.entries_full(None, None).unwrap().into_iter().next().unwrap(); - let e = db.entries_full(None, None).unwrap().into_iter().next().unwrap(); + assert_eq!(e.end, Some(facts.now)); - assert_eq!(e.end, Some(now)); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Checked out of sheet \"default\".\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Checked out of sheet \"default\".\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn tells_if_no_running_entry() { - let mut db = SqliteDatabase::from_memory().unwrap(); let args = Default::default(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - db.init().unwrap(); + streams.db.entry_insert(facts.now, None, None, "non-default").unwrap(); - db.entry_insert(now, None, None, "non-default").unwrap(); + OutCommand::handle(args, &mut streams, &facts).unwrap(); - OutCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("No running entry on sheet \"default\".\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("No running entry on sheet \"default\".\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn warns_if_old_database() { - let mut db = SqliteDatabase::from_memory().unwrap(); let args = Default::default(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake_old(b""); + let facts = Facts::new(); - db.init_old().unwrap(); + streams.db.entry_insert(facts.now, None, None, "default").unwrap(); - db.entry_insert(now, None, None, "default").unwrap(); + OutCommand::handle(args, &mut streams, &facts).unwrap(); - OutCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); + let e = streams.db.entries_full(None, None).unwrap().into_iter().next().unwrap(); - let e = db.entries_full(None, None).unwrap().into_iter().next().unwrap(); + assert_eq!(e.end, Some(Utc.from_utc_datetime(&facts.now.with_timezone(&Local).naive_local()))); - assert_eq!(e.end, Some(Utc.from_utc_datetime(&now.with_timezone(&Local).naive_local()))); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Checked out of sheet \"default\".\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps(&format!( - "{} You are using the old timetrap format, it is advised that \ - you update your database using t migrate\n", - Yellow.bold().paint("[WARNING]")))); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Checked out of sheet \"default\".\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps( + "[WARNING] You are using the old timetrap format, it is advised that \ + you update your database using t migrate\n")); } } diff --git a/src/commands/resume.rs b/src/commands/resume.rs index 16b50e7..c9ba47d 100644 --- a/src/commands/resume.rs +++ b/src/commands/resume.rs @@ -1,16 +1,16 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; use chrono::{DateTime, Utc}; use crate::error::{Error, Result}; use crate::timeparse::parse_time; -use crate::config::Config; use crate::database::Database; use crate::models::Entry; +use crate::io::Streams; -use super::{Command, r#in, sheet}; +use super::{Command, Facts, r#in, sheet}; #[derive(Default)] pub struct Args { @@ -29,14 +29,15 @@ impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { } } -fn resume(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, entry: Entry, now: DateTime) -> Result<()> +fn resume(args: Args, streams: &mut Streams, facts: &Facts, entry: Entry) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { writeln!( - out, + streams.out, "Resuming \"{}\" from entry #{}", entry.note.clone().unwrap_or_else(|| "".to_owned()), entry.id )?; @@ -44,7 +45,7 @@ where r#in::InCommand::handle(r#in::Args { at: args.at, note: entry.note, - }, db, out, err, config, now) + }, streams, facts) } pub struct ResumeCommand; @@ -52,27 +53,28 @@ pub struct ResumeCommand; impl<'a> Command<'a> for ResumeCommand { type Args = Args; - fn handle(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let current_sheet = db.current_sheet()?.unwrap_or_else(|| "default".to_owned()); + let current_sheet = streams.db.current_sheet()?.unwrap_or_else(|| "default".to_owned()); // First try to process using the given id if let Some(entry_id) = args.id { - if let Some(entry) = db.entry_by_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()), - }, db, out, err, config, now)?; + }, streams, facts)?; } - return resume(args, db, out, err, config, entry, now); + return resume(args, streams, facts, entry); } else { - writeln!(out, "The entry with id '{}' could not be found to be resumed. Perhaps it was deleted?", entry_id)?; + writeln!(streams.out, "The entry with id '{}' could not be found to be resumed. Perhaps it was deleted?", entry_id)?; return Ok(()); } @@ -80,10 +82,10 @@ impl<'a> Command<'a> for ResumeCommand { // No id specified, try to find something suitable to switch to in the // database - if let Some(entry) = db.last_checkout_of_sheet(¤t_sheet)? { - resume(args ,db, out, err, config, entry, now) + if let Some(entry) = streams.db.last_checkout_of_sheet(¤t_sheet)? { + resume(args, streams, facts, entry) } else { - writeln!(out, "No entry to resume in the sheet '{}'. Perhaps start a new one? + writeln!(streams.out, "No entry to resume in the sheet '{}'. Perhaps start a new one? Hint: use t in", current_sheet)?; Ok(()) @@ -97,56 +99,47 @@ mod tests { use pretty_assertions::assert_eq; use crate::test_utils::Ps; - use crate::database::SqliteDatabase; use super::*; #[test] fn resume_an_entry() { let args = Default::default(); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); - let one_hour_ago = now - Duration::hours(1); - let two_hours_ago = now - Duration::hours(2); + 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); - db.init().unwrap(); + streams.db.entry_insert(two_hours_ago, Some(one_hour_ago), Some("fake note".into()), "default").unwrap(); - 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); - assert_eq!(db.entries_full(None, None).unwrap().len(), 1); + ResumeCommand::handle(args, &mut streams, &facts).unwrap(); - ResumeCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - let all_entries = db.entries_full(None, None).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, now); + 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_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Resuming \"fake note\" from entry #1 + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Resuming \"fake note\" from entry #1 Checked into sheet \"default\".\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] fn no_entries_to_resume() { let args = Default::default(); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - db.init().unwrap(); + ResumeCommand::handle(args, &mut streams, &facts).unwrap(); - ResumeCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("\ + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("\ No entry to resume in the sheet 'default'. Perhaps start a new one? Hint: use t in ")); diff --git a/src/commands/sheet.rs b/src/commands/sheet.rs index 2d47b95..4548790 100644 --- a/src/commands/sheet.rs +++ b/src/commands/sheet.rs @@ -1,14 +1,13 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; -use chrono::{DateTime, Utc}; use crate::database::Database; use crate::error::{Error, Result}; -use crate::commands::Command; -use crate::config::Config; +use crate::commands::{Command, Facts}; use crate::commands::list::ListCommand; +use crate::io::Streams; #[derive(Default)] pub struct Args { @@ -30,42 +29,43 @@ pub struct SheetCommand {} impl<'a> Command<'a> for SheetCommand { type Args = Args; - fn handle(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { if let Some(sheet) = args.sheet { - let current_sheet = db.current_sheet()?.unwrap_or_else(|| "default".into()); + let current_sheet = streams.db.current_sheet()?.unwrap_or_else(|| "default".into()); // sheet given, switch to it let move_to = if sheet == "-" { - if let Some(move_to) = db.last_sheet()? { + if let Some(move_to) = streams.db.last_sheet()? { move_to } else { - writeln!(out, "No previous sheet to move to. Staying on '{}'. + writeln!(streams.out, "No previous sheet to move to. Staying on '{}'. Hint: remember that giving - (a dash) as argument to t sheet switches to the last active sheet", current_sheet)?; return Ok(()); } } else if sheet == current_sheet { - writeln!(out, "Already on sheet '{}'", sheet)?; + writeln!(streams.out, "Already on sheet '{}'", sheet)?; return Ok(()); } else { sheet }; - db.set_last_sheet(¤t_sheet)?; - db.set_current_sheet(&move_to)?; + streams.db.set_last_sheet(¤t_sheet)?; + streams.db.set_current_sheet(&move_to)?; - writeln!(out, "Switching to sheet '{}'", move_to)?; + writeln!(streams.out, "Switching to sheet '{}'", move_to)?; Ok(()) } else { // call list - ListCommand::handle(Default::default(), db, out, err, config, now) + ListCommand::handle(Default::default(), streams, facts) } } } @@ -74,7 +74,6 @@ Hint: remember that giving - (a dash) as argument to t sheet switches to the las mod tests { use pretty_assertions::assert_eq; - use crate::database::SqliteDatabase; use crate::test_utils::Ps; use super::*; @@ -84,18 +83,14 @@ mod tests { let args = Args { sheet: Some("new_sheet".into()), }; - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - db.init().unwrap(); + SheetCommand::handle(args, &mut streams, &facts).unwrap(); - SheetCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); - - assert_eq!(db.current_sheet().unwrap().unwrap(), "new_sheet"); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Switching to sheet 'new_sheet'\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(streams.db.current_sheet().unwrap().unwrap(), "new_sheet"); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Switching to sheet 'new_sheet'\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] @@ -103,19 +98,16 @@ mod tests { let args = Args { sheet: Some("foo".into()), }; - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - db.init().unwrap(); - db.set_current_sheet("foo").unwrap(); + streams.db.set_current_sheet("foo").unwrap(); - SheetCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); + SheetCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(db.current_sheet().unwrap().unwrap(), "foo"); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Already on sheet 'foo'\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(streams.db.current_sheet().unwrap().unwrap(), "foo"); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Already on sheet 'foo'\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } #[test] @@ -123,20 +115,17 @@ mod tests { let args = Args { sheet: Some("-".into()), }; - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let now = Utc::now(); + let mut streams = Streams::fake(b""); + let facts = Facts::new(); - db.init().unwrap(); - db.set_current_sheet("foo").unwrap(); - db.set_last_sheet("var").unwrap(); + streams.db.set_current_sheet("foo").unwrap(); + streams.db.set_last_sheet("var").unwrap(); - SheetCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); + SheetCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(db.current_sheet().unwrap().unwrap(), "var"); - assert_eq!(db.last_sheet().unwrap().unwrap(), "foo"); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Switching to sheet 'var'\n")); - assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); + assert_eq!(streams.db.current_sheet().unwrap().unwrap(), "var"); + assert_eq!(streams.db.last_sheet().unwrap().unwrap(), "foo"); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps("Switching to sheet 'var'\n")); + assert_eq!(Ps(&String::from_utf8_lossy(&streams.err)), Ps("")); } } diff --git a/src/commands/today.rs b/src/commands/today.rs index ff45999..d663085 100644 --- a/src/commands/today.rs +++ b/src/commands/today.rs @@ -1,5 +1,5 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; use chrono::{DateTime, Utc, Local}; @@ -8,11 +8,11 @@ use regex::Regex; use crate::error::{Result, Error}; use crate::database::Database; use crate::formatters::Formatter; -use crate::config::Config; use crate::timeparse::parse_time; use crate::regex::parse_regex; +use crate::io::Streams; -use super::{Command, display::{Sheet, entries_for_display}}; +use super::{Command, Facts, display::{Sheet, entries_for_display}}; #[derive(Default)] pub struct Args { @@ -42,15 +42,25 @@ pub struct TodayCommand { } impl<'a> Command<'a> for TodayCommand { type Args = Args; - fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let start = Some(now.with_timezone(&Local).date().and_hms(0, 0, 0).with_timezone(&Utc)); + let start = Some(facts.now.with_timezone(&Local).date().and_hms(0, 0, 0).with_timezone(&Utc)); - entries_for_display(start, args.end, args.sheet, db, out, err, args.format.unwrap_or_else(|| config.default_formatter.clone()), args.ids, args.grep, now) + entries_for_display( + start, + args.end, + args.sheet, + streams, + args.format.unwrap_or_else(|| facts.config.default_formatter.clone()), + args.ids, + args.grep, + facts + ) } } @@ -58,7 +68,7 @@ impl<'a> Command<'a> for TodayCommand { mod tests { use chrono::TimeZone; - use crate::database::SqliteDatabase; + use crate::config::Config; use super::*; @@ -67,22 +77,18 @@ mod tests { std::env::set_var("TZ", "CST+6"); let args = Default::default(); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Config { + let mut streams = Streams::fake(b""); + let facts = Facts::new().with_config(Config { default_formatter: Formatter::Ids, ..Default::default() - }; + }).with_now(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); + TodayCommand::handle(args, &mut streams, &facts).unwrap(); - TodayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)).unwrap(); - - assert_eq!(&String::from_utf8_lossy(&out), "1 2\n"); - assert_eq!(String::from_utf8_lossy(&err), ""); + assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n"); + assert_eq!(String::from_utf8_lossy(&streams.err), ""); } } diff --git a/src/commands/week.rs b/src/commands/week.rs index 95564d6..aded438 100644 --- a/src/commands/week.rs +++ b/src/commands/week.rs @@ -1,5 +1,5 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; use chrono::{DateTime, Utc, Local, Duration, Weekday, Datelike}; @@ -8,11 +8,12 @@ use regex::Regex; use crate::error::{Result, Error}; use crate::database::Database; use crate::formatters::Formatter; -use crate::config::{Config, WeekDay}; +use crate::config::WeekDay; use crate::regex::parse_regex; use crate::timeparse::parse_time; +use crate::io::Streams; -use super::{Command, display::{Sheet, entries_for_display}}; +use super::{Command, Facts, display::{Sheet, entries_for_display}}; trait AsNum { fn as_num(&self) -> i64; @@ -86,15 +87,25 @@ pub struct WeekCommand { } impl<'a> Command<'a> for WeekCommand { type Args = Args; - fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let start = prev_day(now.with_timezone(&Local), config.week_start); + let start = prev_day(facts.now.with_timezone(&Local), facts.config.week_start); - entries_for_display(Some(start), args.end, args.sheet, db, out, err, args.format.unwrap_or_else(|| config.default_formatter.clone()), args.ids, args.grep, now) + entries_for_display( + Some(start), + args.end, + args.sheet, + streams, + args.format.unwrap_or_else(|| facts.config.default_formatter.clone()), + args.ids, + args.grep, + facts + ) } } @@ -102,7 +113,7 @@ impl<'a> Command<'a> for WeekCommand { mod tests { use chrono::TimeZone; - use crate::database::SqliteDatabase; + use crate::config::Config; use super::*; @@ -125,22 +136,19 @@ mod tests { std::env::set_var("TZ", "CST+6"); let args = Default::default(); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Config { + let mut streams = Streams::fake(b""); + let now = Utc.ymd(2021, 7, 1).and_hms(10, 0, 0); + let facts = Facts::new().with_config(Config { default_formatter: Formatter::Ids, ..Default::default() - }; + }).with_now(now); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); + WeekCommand::handle(args, &mut streams, &facts).unwrap(); - WeekCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc.ymd(2021, 7, 1).and_hms(10, 0, 0)).unwrap(); - - assert_eq!(&String::from_utf8_lossy(&out), "1 2\n"); - assert_eq!(String::from_utf8_lossy(&err), ""); + assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n"); + assert_eq!(String::from_utf8_lossy(&streams.err), ""); } } diff --git a/src/commands/yesterday.rs b/src/commands/yesterday.rs index 878c6ae..04f6989 100644 --- a/src/commands/yesterday.rs +++ b/src/commands/yesterday.rs @@ -1,17 +1,17 @@ use std::convert::TryFrom; -use std::io::Write; +use std::io::{BufRead, Write}; use clap::ArgMatches; -use chrono::{DateTime, Utc, Local, Duration}; +use chrono::{Utc, Local, Duration}; use regex::Regex; use crate::error::{Result, Error}; use crate::database::Database; use crate::formatters::Formatter; -use crate::config::Config; use crate::regex::parse_regex; +use crate::io::Streams; -use super::{Command, display::{Sheet, entries_for_display}}; +use super::{Command, Facts, display::{Sheet, entries_for_display}}; #[derive(Default)] pub struct Args { @@ -39,17 +39,27 @@ pub struct YesterdayCommand { } impl<'a> Command<'a> for YesterdayCommand { type Args = Args; - fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> + fn handle(args: Self::Args, streams: &mut Streams, facts: &Facts) -> Result<()> where D: Database, + I: BufRead, O: Write, E: Write, { - let today = now.with_timezone(&Local).date(); + let today = facts.now.with_timezone(&Local).date(); let start = Some((today - Duration::days(1)).and_hms(0, 0, 0).with_timezone(&Utc)); let end = Some(today.and_hms(0, 0, 0).with_timezone(&Utc)); - entries_for_display(start, end, args.sheet, db, out, err, args.format.unwrap_or_else(|| config.default_formatter.clone()), args.ids, args.grep, now) + entries_for_display( + start, + end, + args.sheet, + streams, + args.format.unwrap_or_else(|| facts.config.default_formatter.clone()), + args.ids, + args.grep, + facts + ) } } @@ -58,8 +68,8 @@ mod tests { use chrono::{Duration, TimeZone}; use pretty_assertions::assert_eq; - use crate::database::SqliteDatabase; use crate::test_utils::Ps; + use crate::config::Config; use super::*; @@ -69,29 +79,24 @@ mod tests { format: Some(Formatter::Csv), ..Default::default() }; - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Default::default(); - - db.init().unwrap(); - + let mut streams = Streams::fake(b""); let two_days_ago = Local::now().date() - Duration::days(2); let yesterday = Local::now().date() - Duration::days(1); let today = Local::now().date(); + let facts = Facts::new(); - db.entry_insert(two_days_ago.and_hms(1, 2, 3).with_timezone(&Utc), None, None, "default".into()).unwrap(); - db.entry_insert(yesterday.and_hms(1, 2, 3).with_timezone(&Utc), None, Some("This!".into()), "default".into()).unwrap(); - db.entry_insert(today.and_hms(1, 2, 3).with_timezone(&Utc), None, None, "default".into()).unwrap(); + streams.db.entry_insert(two_days_ago.and_hms(1, 2, 3).with_timezone(&Utc), None, None, "default".into()).unwrap(); + streams.db.entry_insert(yesterday.and_hms(1, 2, 3).with_timezone(&Utc), None, Some("This!".into()), "default".into()).unwrap(); + streams.db.entry_insert(today.and_hms(1, 2, 3).with_timezone(&Utc), None, None, "default".into()).unwrap(); - YesterdayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); + YesterdayCommand::handle(args, &mut streams, &facts).unwrap(); - assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps(&format!("start,end,note,sheet + assert_eq!(Ps(&String::from_utf8_lossy(&streams.out)), Ps(&format!("start,end,note,sheet {},,This!,default ", yesterday.and_hms(1, 2, 3).with_timezone(&Utc).to_rfc3339_opts(chrono::SecondsFormat::Micros, true)))); assert_eq!( - String::from_utf8_lossy(&err), + String::from_utf8_lossy(&streams.err), String::new(), ); } @@ -101,22 +106,19 @@ mod tests { std::env::set_var("TZ", "CST+6"); let args = Default::default(); - let mut db = SqliteDatabase::from_memory().unwrap(); - let mut out = Vec::new(); - let mut err = Vec::new(); - let config = Config { + let mut streams = Streams::fake(b""); + let now = Utc.ymd(2021, 7, 1).and_hms(10, 0, 0); + let facts = Facts::new().with_config(Config { default_formatter: Formatter::Ids, ..Default::default() - }; + }).with_now(now); - db.init().unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); + streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); - db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); + YesterdayCommand::handle(args, &mut streams, &facts).unwrap(); - YesterdayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc.ymd(2021, 7, 1).and_hms(10, 0, 0)).unwrap(); - - assert_eq!(&String::from_utf8_lossy(&out), "1 2\n"); - assert_eq!(String::from_utf8_lossy(&err), ""); + assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n"); + assert_eq!(String::from_utf8_lossy(&streams.err), ""); } } diff --git a/src/config.rs b/src/config.rs index a7acfc9..6e5f44b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,3 @@ -use std::env; use std::path::{Path, PathBuf}; use std::fs::{File, create_dir_all}; use std::io::{Read, Write}; @@ -90,9 +89,9 @@ pub struct Config { impl Config { /// Tries as hard as possible to read the current configuration. Retrieving /// the path to it from the environment or common locations. - pub fn read() -> Result { + pub fn read(timetrap_config_file: Option<&str>) -> Result { // first try from env variable TIMETRAP_CONFIG_FILE - if let Ok(value) = env::var("TIMETRAP_CONFIG_FILE") { + if let Some(value) = timetrap_config_file { return if value.ends_with(".toml") { let config_path = PathBuf::from(&value); diff --git a/src/database.rs b/src/database.rs index 57fdb3c..8eb405d 100644 --- a/src/database.rs +++ b/src/database.rs @@ -246,19 +246,19 @@ pub struct SqliteDatabase { } impl SqliteDatabase { - pub fn from_memory() -> Result { + pub fn from_memory() -> Result { Ok(SqliteDatabase { connection: Connection::open_in_memory()?, }) } - pub fn from_path>(path: P) -> Result { + pub fn from_path>(path: P) -> Result { Ok(SqliteDatabase { connection: Connection::open(path)?, }) } - pub fn from_path_or_create>(path: P) -> Result { + pub fn from_path_or_create>(path: P) -> Result { if path.as_ref().is_file() { Self::from_path(path) } else { diff --git a/src/env.rs b/src/env.rs new file mode 100644 index 0000000..b788706 --- /dev/null +++ b/src/env.rs @@ -0,0 +1,39 @@ +use std::env; + +use atty::Stream; +#[cfg(windows)] +use ansi_term::enable_ansi_support; + +/// Reads the given environment variable and decides if its value is true or +/// false +fn bool_env(name: &str) -> bool { + if let Some(value) = env::var_os(name) { + !(value == "0" || value == "false" || value == "") + } else { + false + } +} + +#[derive(Clone, Default)] +pub struct Env { + pub timetrap_config_file: Option, + pub supress_warming: bool, + pub stdout_is_tty: bool, + pub stderr_is_tty: bool, +} + +impl Env { + pub fn read() -> Env { + #[cfg(windows)] + let tty = enable_ansi_support().is_some(); + #[cfg(not(windows))] + let tty = true; + + Env { + timetrap_config_file: env::var("TIMETRAP_CONFIG_FILE").ok(), + supress_warming: bool_env("TIEMPO_SUPRESS_TIMETRAP_WARNING"), + stdout_is_tty: tty && atty::is(Stream::Stdout), + stderr_is_tty: tty && atty::is(Stream::Stderr), + } + } +} diff --git a/src/interactive.rs b/src/interactive.rs index dcdb8bd..ce1bf99 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -1,14 +1,17 @@ -use std::io::{self, Write}; +use std::io::{self, BufRead, Write}; -fn read_line() -> io::Result { +use crate::io::Streams; +use crate::database::Database; + +fn read_line(mut r#in: I) -> io::Result { let mut pre_n = String::new(); - io::stdin().read_line(&mut pre_n)?; + r#in.read_line(&mut pre_n)?; Ok(pre_n) } -pub fn ask(out: &mut W, question: &str) -> io::Result { - write!(out, "{} [y/N] ", question)?; - out.flush()?; +pub fn ask(streams: &mut Streams, question: &str) -> io::Result { + write!(streams.out, "{} [y/N] ", question)?; + streams.out.flush()?; - Ok(read_line()?.to_lowercase().starts_with('y')) + Ok(read_line(&mut streams.r#in)?.to_lowercase().starts_with('y')) } diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..b714f7e --- /dev/null +++ b/src/io.rs @@ -0,0 +1,56 @@ +use std::io::{BufRead, Write}; + +use crate::database::{Database, SqliteDatabase}; + +pub struct Streams +where + D: Database, + I: BufRead, + O: Write, + E: Write, +{ + pub db: D, + pub r#in: I, + pub out: O, + pub err: E, +} + +impl Streams, Vec> { + pub fn fake(r#in: &[u8]) -> Streams, Vec> { + let mut db = SqliteDatabase::from_memory().unwrap(); + + db.init().unwrap(); + + Streams { + db, + r#in, + out: Vec::new(), + err: Vec::new(), + } + } + + pub fn fake_old(r#in: &[u8]) -> Streams, Vec> { + let mut db = SqliteDatabase::from_memory().unwrap(); + + db.init_old().unwrap(); + + Streams { + db, + r#in, + out: Vec::new(), + err: Vec::new(), + } + } + + pub fn with_db(self, db: SqliteDatabase) -> Self { + Streams { + db, + ..self + } + } + + pub fn reset_io(&mut self) { + self.out = Vec::new(); + self.err = Vec::new(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 11ec9c6..fafcd3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ pub mod regex; pub mod tabulate; pub mod old; pub mod interactive; +pub mod env; +pub mod io; #[cfg(test)] pub mod test_utils; diff --git a/src/main.rs b/src/main.rs index 560657e..52390ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,46 +11,55 @@ use regex::Regex; use tiempo::error; use tiempo::database::SqliteDatabase; +use tiempo::env::Env; use tiempo::config::Config; use tiempo::commands::{ - Command, r#in::InCommand, display::DisplayCommand, sheet::SheetCommand, - today::TodayCommand, yesterday::YesterdayCommand, week::WeekCommand, - month::MonthCommand, list::ListCommand, out::OutCommand, + Command, Facts, r#in::InCommand, display::DisplayCommand, + sheet::SheetCommand, today::TodayCommand, yesterday::YesterdayCommand, + week::WeekCommand, month::MonthCommand, list::ListCommand, out::OutCommand, resume::ResumeCommand, backend::BackendCommand, kill::KillCommand, now::NowCommand, edit::EditCommand, archive::ArchiveCommand, configure::ConfigureCommand, }; +use tiempo::io::Streams; fn error_trap(matches: ArgMatches) -> error::Result<()> { - let config = Config::read()?; + let env = Env::read(); + let facts = Facts { + config: Config::read(env.timetrap_config_file.as_deref())?, + env, + now: Utc::now(), + }; if let Some(_matches) = matches.subcommand_matches("backend") { - return BackendCommand::handle(&config); + return BackendCommand::handle(&facts.config); } - let mut conn = SqliteDatabase::from_path_or_create(&config.database_file)?; - let mut out = io::stdout(); - let mut err = io::stderr(); - let now = Utc::now(); + let mut streams = Streams { + db: SqliteDatabase::from_path_or_create(&facts.config.database_file)?, + r#in: io::BufReader::new(io::stdin()), + out: io::stdout(), + err: io::stderr(), + }; match matches.subcommand() { - ("in", Some(matches)) => InCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("out", Some(matches)) => OutCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("resume", Some(matches)) => ResumeCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), + ("in", Some(matches)) => InCommand::handle(matches.try_into()?, &mut streams, &facts), + ("out", Some(matches)) => OutCommand::handle(matches.try_into()?, &mut streams, &facts), + ("resume", Some(matches)) => ResumeCommand::handle(matches.try_into()?, &mut streams, &facts), - ("display", Some(matches)) => DisplayCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("today", Some(matches)) => TodayCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("yesterday", Some(matches)) => YesterdayCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("week", Some(matches)) => WeekCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("month", Some(matches)) => MonthCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), + ("display", Some(matches)) => DisplayCommand::handle(matches.try_into()?, &mut streams, &facts), + ("today", Some(matches)) => TodayCommand::handle(matches.try_into()?, &mut streams, &facts), + ("yesterday", Some(matches)) => YesterdayCommand::handle(matches.try_into()?, &mut streams, &facts), + ("week", Some(matches)) => WeekCommand::handle(matches.try_into()?, &mut streams, &facts), + ("month", Some(matches)) => MonthCommand::handle(matches.try_into()?, &mut streams, &facts), - ("sheet", Some(matches)) => SheetCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("list", Some(matches)) => ListCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("kill", Some(matches)) => KillCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("now", Some(matches)) => NowCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("edit", Some(matches)) => EditCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("archive", Some(matches)) => ArchiveCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), - ("configure", Some(matches)) => ConfigureCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now), + ("sheet", Some(matches)) => SheetCommand::handle(matches.try_into()?, &mut streams, &facts), + ("list", Some(matches)) => ListCommand::handle(matches.try_into()?, &mut streams, &facts), + ("kill", Some(matches)) => KillCommand::handle(matches.try_into()?, &mut streams, &facts), + ("now", Some(matches)) => NowCommand::handle(matches.try_into()?, &mut streams, &facts), + ("edit", Some(matches)) => EditCommand::handle(matches.try_into()?, &mut streams, &facts), + ("archive", Some(matches)) => ArchiveCommand::handle(matches.try_into()?, &mut streams, &facts), + ("configure", Some(matches)) => ConfigureCommand::handle(matches.try_into()?, &mut streams, &facts), (cmd, _) => Err(error::Error::UnimplementedCommand(cmd.into())), } diff --git a/src/old.rs b/src/old.rs index a724bee..43aaee2 100644 --- a/src/old.rs +++ b/src/old.rs @@ -1,11 +1,12 @@ use std::io::Write; use chrono::{DateTime, Utc, Local, LocalResult, TimeZone}; -use ansi_term::Color::Yellow; +use ansi_term::{Color::Yellow, Style}; use crate::error::{Error::*, Result}; use crate::models::Entry; use crate::database::{Database, DBVersion}; +use crate::env::Env; /// Treat t as if it wasnt actually in Utc but in the local timezone and return /// the actual Utc time. @@ -72,13 +73,17 @@ pub fn time_or_warning(time: DateTime, db: &D) -> Result<(Date } /// emits the appropiate warning if the old database format was detected. -pub fn warn_if_needed(err: &mut E, needs_warning: bool) -> Result<()> { - if needs_warning && std::env::var_os("TIEMPO_SUPRESS_TIMETRAP_WARNING").is_none() { +pub fn warn_if_needed(err: &mut E, needs_warning: bool, env: &Env) -> Result<()> { + if needs_warning && !env.supress_warming { writeln!( err, "{} You are using the old timetrap format, it is advised that \ you update your database using t migrate", - Yellow.bold().paint("[WARNING]"), + if env.stderr_is_tty { + Yellow.bold().paint("[WARNING]") + } else { + Style::new().paint("[WARNING]") + }, ).map_err(IOError)?; }