Command trait redesing

This commit is contained in:
Abraham Toriz 2021-08-25 14:30:27 -05:00
parent 9bb4b6d6ba
commit ba632c88a3
26 changed files with 754 additions and 660 deletions

1
Cargo.lock generated
View File

@ -623,6 +623,7 @@ name = "tiempo"
version = "1.0.4"
dependencies = [
"ansi_term 0.12.1",
"atty",
"chrono",
"clap",
"csv",

View File

@ -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"

View File

@ -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<Utc>,
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<Utc>) -> 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<D: Database, O: Write, E: Write>(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>;
fn handle<D: Database, I: BufRead, O: Write, E: Write>(args: Self::Args, streams: &mut Streams<D, I, O, E>, facts: &Facts) -> Result<()>;
}

View File

@ -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<D, O, E>(args: Args, db: &mut D, out: &mut O, _err: &mut E, _config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, 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(())

View File

@ -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<D, O, E>(args: Args, _db: &mut D, out: &mut O, _err: &mut E, config: &Config, _now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, 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(())

View File

@ -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<D, O, E>(
pub fn entries_for_display<D, I, O, E>(
start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>,
sheet: Option<Sheet>, db: &mut D, out: &mut O, err: &mut E,
sheet: Option<Sheet>, streams: &mut Streams<D, I, O, E>,
format: Formatter, ids: bool, grep: Option<Regex>,
now: DateTime<Utc>,
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(&current_sheet, start, end)?
streams.db.entries_by_sheet(&current_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<D, O, E>(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Self::Args, streams: &mut Streams<D, I, O, E>, 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), "");
}
}

View File

@ -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<D, O, E>(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, 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(&current_sheet)? {
} else if let Some(entry) = streams.db.last_entry_of_sheet(&current_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);
}
}

View File

@ -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<D, O, E>(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, 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(""));
}
}

View File

@ -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<D, O, E>(args: Args, db: &mut D, out: &mut O, _err: &mut E, _config: &Config, _now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, _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")?;
}
}
}

View File

@ -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<D, O, E>(args: Args, db: &mut D, out: &mut O, err: &mut E, _config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, 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"
);
}
}

View File

@ -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<Local>) -> DateTime<Utc> {
@ -93,12 +93,14 @@ pub struct MonthCommand { }
impl<'a> Command<'a> for MonthCommand {
type Args = Args;
fn handle<D, O, E>(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Self::Args, streams: &mut Streams<D, I, O, E>, 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), "");
}
}

View File

@ -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<D, O, E>(_args: Self::Args, db: &mut D, out: &mut O, err: &mut E, _config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(_args: Self::Args, streams: &mut Streams<D, I, O, E>, 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"
);
}
}

View File

@ -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<D: Database, O: Write, E: Write>(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, _config: &Config, now: DateTime<Utc>) -> Result<()> {
let end = args.at.unwrap_or(now);
let sheet = db.current_sheet()?.unwrap_or_else(|| "default".into());
fn handle<D, I, O, E>(args: Self::Args, streams: &mut Streams<D, I, O, E>, 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"));
}
}

View File

@ -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<D, O, E>(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, entry: Entry, now: DateTime<Utc>) -> Result<()>
fn resume<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, 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<D, O, E>(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, 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(&current_sheet)? {
resume(args ,db, out, err, config, entry, now)
if let Some(entry) = streams.db.last_checkout_of_sheet(&current_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
"));

View File

@ -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<D, O, E>(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Args, streams: &mut Streams<D, I, O, E>, 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(&current_sheet)?;
db.set_current_sheet(&move_to)?;
streams.db.set_last_sheet(&current_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(""));
}
}

View File

@ -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<D, O, E>(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Self::Args, streams: &mut Streams<D, I, O, E>, 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), "");
}
}

View File

@ -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<D, O, E>(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Self::Args, streams: &mut Streams<D, I, O, E>, 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), "");
}
}

View File

@ -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<D, O, E>(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime<Utc>) -> Result<()>
fn handle<D, I, O, E>(args: Self::Args, streams: &mut Streams<D, I, O, E>, 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), "");
}
}

View File

@ -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<Config> {
pub fn read(timetrap_config_file: Option<&str>) -> Result<Config> {
// 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);

View File

@ -246,19 +246,19 @@ pub struct SqliteDatabase {
}
impl SqliteDatabase {
pub fn from_memory() -> Result<impl Database> {
pub fn from_memory() -> Result<SqliteDatabase> {
Ok(SqliteDatabase {
connection: Connection::open_in_memory()?,
})
}
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<impl Database> {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<SqliteDatabase> {
Ok(SqliteDatabase {
connection: Connection::open(path)?,
})
}
pub fn from_path_or_create<P: AsRef<Path>>(path: P) -> Result<impl Database> {
pub fn from_path_or_create<P: AsRef<Path>>(path: P) -> Result<SqliteDatabase> {
if path.as_ref().is_file() {
Self::from_path(path)
} else {

39
src/env.rs Normal file
View File

@ -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<String>,
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),
}
}
}

View File

@ -1,14 +1,17 @@
use std::io::{self, Write};
use std::io::{self, BufRead, Write};
fn read_line() -> io::Result<String> {
use crate::io::Streams;
use crate::database::Database;
fn read_line<I: BufRead>(mut r#in: I) -> io::Result<String> {
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<W: Write>(out: &mut W, question: &str) -> io::Result<bool> {
write!(out, "{} [y/N] ", question)?;
out.flush()?;
pub fn ask<D: Database, I: BufRead, O: Write, E: Write>(streams: &mut Streams<D, I, O, E>, question: &str) -> io::Result<bool> {
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'))
}

56
src/io.rs Normal file
View File

@ -0,0 +1,56 @@
use std::io::{BufRead, Write};
use crate::database::{Database, SqliteDatabase};
pub struct Streams<D, I, O, E>
where
D: Database,
I: BufRead,
O: Write,
E: Write,
{
pub db: D,
pub r#in: I,
pub out: O,
pub err: E,
}
impl Streams<SqliteDatabase, &[u8], Vec<u8>, Vec<u8>> {
pub fn fake(r#in: &[u8]) -> Streams<SqliteDatabase, &[u8], Vec<u8>, Vec<u8>> {
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<SqliteDatabase, &[u8], Vec<u8>, Vec<u8>> {
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();
}
}

View File

@ -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;

View File

@ -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())),
}

View File

@ -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<D: Database>(time: DateTime<Utc>, db: &D) -> Result<(Date
}
/// emits the appropiate warning if the old database format was detected.
pub fn warn_if_needed<E: Write>(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<E: Write>(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)?;
}