diff --git a/Cargo.lock b/Cargo.lock index a0786badd..b8549ae21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2274,6 +2274,7 @@ dependencies = [ "prettytable-rs", "taskchampion", "tempdir", + "termcolor", "textwrap 0.12.1", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 949896a20..a243a5d44 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -12,6 +12,7 @@ log = "^0.4.11" nom = "*" prettytable-rs = "^0.8.0" textwrap = "0.12.1" +termcolor = "1.1.2" [dependencies.config] default-features = false diff --git a/cli/src/invocation/cmd/add.rs b/cli/src/invocation/cmd/add.rs index 94ac03227..79cb5dd4b 100644 --- a/cli/src/invocation/cmd/add.rs +++ b/cli/src/invocation/cmd/add.rs @@ -1,34 +1,42 @@ use crate::argparse::{DescriptionMod, Modification}; use failure::Fallible; use taskchampion::{Replica, Status}; +use termcolor::WriteColor; -pub(crate) fn execute(replica: &mut Replica, modification: Modification) -> Fallible<()> { +pub(crate) fn execute( + w: &mut W, + replica: &mut Replica, + modification: Modification, +) -> Fallible<()> { let description = match modification.description { DescriptionMod::Set(ref s) => s.clone(), _ => "(no description)".to_owned(), }; let t = replica.new_task(Status::Pending, description).unwrap(); - println!("added task {}", t.get_uuid()); + write!(w, "added task {}\n", t.get_uuid())?; Ok(()) } #[cfg(test)] mod test { use super::*; - use crate::invocation::cmd::test::test_replica; + use crate::invocation::cmd::test::*; #[test] fn test_add() { + let mut w = test_writer(); let mut replica = test_replica(); let modification = Modification { description: DescriptionMod::Set("my description".to_owned()), ..Default::default() }; - execute(&mut replica, modification).unwrap(); + execute(&mut w, &mut replica, modification).unwrap(); // check that the task appeared.. let task = replica.get_working_set_task(1).unwrap().unwrap(); assert_eq!(task.get_description(), "my description"); assert_eq!(task.get_status(), Status::Pending); + + assert_eq!(w.into_string(), format!("added task {}\n", task.get_uuid())); } } diff --git a/cli/src/invocation/cmd/gc.rs b/cli/src/invocation/cmd/gc.rs index 0898c5b1b..644974eb5 100644 --- a/cli/src/invocation/cmd/gc.rs +++ b/cli/src/invocation/cmd/gc.rs @@ -1,21 +1,23 @@ use failure::Fallible; use taskchampion::Replica; +use termcolor::WriteColor; -pub(crate) fn execute(replica: &mut Replica) -> Fallible<()> { +pub(crate) fn execute(w: &mut W, replica: &mut Replica) -> Fallible<()> { replica.gc()?; - println!("garbage collected."); + write!(w, "garbage collected.\n")?; Ok(()) } #[cfg(test)] mod test { use super::*; - use crate::invocation::cmd::test::test_replica; + use crate::invocation::cmd::test::*; #[test] fn test_gc() { + let mut w = test_writer(); let mut replica = test_replica(); - execute(&mut replica).unwrap(); - // this mostly just needs to not fail! + execute(&mut w, &mut replica).unwrap(); + assert_eq!(&w.into_string(), "garbage collected.\n") } } diff --git a/cli/src/invocation/cmd/help.rs b/cli/src/invocation/cmd/help.rs index a53e54897..a95e8415f 100644 --- a/cli/src/invocation/cmd/help.rs +++ b/cli/src/invocation/cmd/help.rs @@ -1,24 +1,31 @@ use crate::usage::Usage; use failure::Fallible; -use std::io; +use termcolor::WriteColor; -pub(crate) fn execute(command_name: String, summary: bool) -> Fallible<()> { +pub(crate) fn execute( + w: &mut W, + command_name: String, + summary: bool, +) -> Fallible<()> { let usage = Usage::new(); - usage.write_help(io::stdout(), command_name, summary)?; + usage.write_help(w, command_name, summary)?; Ok(()) } #[cfg(test)] mod test { use super::*; + use crate::invocation::cmd::test::*; #[test] fn test_summary() { - execute("task".to_owned(), true).unwrap(); + let mut w = test_writer(); + execute(&mut w, "task".to_owned(), true).unwrap(); } #[test] fn test_long() { - execute("task".to_owned(), false).unwrap(); + let mut w = test_writer(); + execute(&mut w, "task".to_owned(), false).unwrap(); } } diff --git a/cli/src/invocation/cmd/info.rs b/cli/src/invocation/cmd/info.rs index 20887587e..852c8faa9 100644 --- a/cli/src/invocation/cmd/info.rs +++ b/cli/src/invocation/cmd/info.rs @@ -4,8 +4,14 @@ use crate::table; use failure::Fallible; use prettytable::{cell, row, Table}; use taskchampion::Replica; +use termcolor::WriteColor; -pub(crate) fn execute(replica: &mut Replica, filter: Filter, debug: bool) -> Fallible<()> { +pub(crate) fn execute( + w: &mut W, + replica: &mut Replica, + filter: Filter, + debug: bool, +) -> Fallible<()> { for task in filtered_tasks(replica, &filter)? { let uuid = task.get_uuid(); @@ -25,7 +31,7 @@ pub(crate) fn execute(replica: &mut Replica, filter: Filter, debug: bool) -> Fal t.add_row(row![b->"Status", task.get_status()]); t.add_row(row![b->"Active", task.is_active()]); } - t.printstd(); + t.print(w)?; } Ok(()) @@ -34,16 +40,22 @@ pub(crate) fn execute(replica: &mut Replica, filter: Filter, debug: bool) -> Fal #[cfg(test)] mod test { use super::*; - use crate::invocation::cmd::test::test_replica; + use crate::invocation::cmd::test::*; + use taskchampion::Status; #[test] fn test_info() { + let mut w = test_writer(); let mut replica = test_replica(); + replica + .new_task(Status::Pending, "my task".to_owned()) + .unwrap(); + let filter = Filter { ..Default::default() }; let debug = false; - execute(&mut replica, filter, debug).unwrap(); - // output is to stdout, so this is as much as we can check + execute(&mut w, &mut replica, filter, debug).unwrap(); + assert!(w.into_string().contains("my task")); } } diff --git a/cli/src/invocation/cmd/list.rs b/cli/src/invocation/cmd/list.rs index 774200f21..4afcdab93 100644 --- a/cli/src/invocation/cmd/list.rs +++ b/cli/src/invocation/cmd/list.rs @@ -4,8 +4,13 @@ use crate::table; use failure::Fallible; use prettytable::{cell, row, Table}; use taskchampion::Replica; +use termcolor::WriteColor; -pub(crate) fn execute(replica: &mut Replica, report: Report) -> Fallible<()> { +pub(crate) fn execute( + w: &mut W, + replica: &mut Replica, + report: Report, +) -> Fallible<()> { let mut t = Table::new(); t.set_format(table::format()); t.set_titles(row![b->"id", b->"act", b->"description"]); @@ -21,7 +26,7 @@ pub(crate) fn execute(replica: &mut Replica, report: Report) -> Fallible<()> { }; t.add_row(row![id, active, task.get_description()]); } - t.printstd(); + t.print(w)?; Ok(()) } @@ -29,17 +34,23 @@ pub(crate) fn execute(replica: &mut Replica, report: Report) -> Fallible<()> { mod test { use super::*; use crate::argparse::Filter; - use crate::invocation::cmd::test::test_replica; + use crate::invocation::cmd::test::*; + use taskchampion::Status; #[test] fn test_list() { + let mut w = test_writer(); let mut replica = test_replica(); + replica + .new_task(Status::Pending, "my task".to_owned()) + .unwrap(); + let report = Report { filter: Filter { ..Default::default() }, }; - execute(&mut replica, report).unwrap(); - // output is to stdout, so this is as much as we can check + execute(&mut w, &mut replica, report).unwrap(); + assert!(w.into_string().contains("my task")); } } diff --git a/cli/src/invocation/cmd/modify.rs b/cli/src/invocation/cmd/modify.rs index 48e122437..dadcd9319 100644 --- a/cli/src/invocation/cmd/modify.rs +++ b/cli/src/invocation/cmd/modify.rs @@ -2,8 +2,10 @@ use crate::argparse::{Filter, Modification}; use crate::invocation::{apply_modification, filtered_tasks}; use failure::Fallible; use taskchampion::Replica; +use termcolor::WriteColor; -pub(crate) fn execute( +pub(crate) fn execute( + w: &mut W, replica: &mut Replica, filter: Filter, modification: Modification, @@ -11,7 +13,7 @@ pub(crate) fn execute( for task in filtered_tasks(replica, &filter)? { let mut task = task.into_mut(replica); - apply_modification(&mut task, &modification)?; + apply_modification(w, &mut task, &modification)?; } Ok(()) @@ -22,10 +24,12 @@ mod test { use super::*; use crate::argparse::DescriptionMod; use crate::invocation::cmd::test::test_replica; + use crate::invocation::cmd::test::*; use taskchampion::Status; #[test] fn test_modify() { + let mut w = test_writer(); let mut replica = test_replica(); let task = replica @@ -39,11 +43,16 @@ mod test { description: DescriptionMod::Set("new description".to_owned()), ..Default::default() }; - execute(&mut replica, filter, modification).unwrap(); + execute(&mut w, &mut replica, filter, modification).unwrap(); // check that the task appeared.. let task = replica.get_task(task.get_uuid()).unwrap().unwrap(); assert_eq!(task.get_description(), "new description"); assert_eq!(task.get_status(), Status::Pending); + + assert_eq!( + w.into_string(), + format!("modified task {}\n", task.get_uuid()) + ); } } diff --git a/cli/src/invocation/cmd/sync.rs b/cli/src/invocation/cmd/sync.rs index 6ce65b88b..737406ae9 100644 --- a/cli/src/invocation/cmd/sync.rs +++ b/cli/src/invocation/cmd/sync.rs @@ -1,26 +1,32 @@ use failure::Fallible; use taskchampion::{server::Server, Replica}; +use termcolor::WriteColor; -pub(crate) fn execute(replica: &mut Replica, server: &mut Box) -> Fallible<()> { +pub(crate) fn execute( + w: &mut W, + replica: &mut Replica, + server: &mut Box, +) -> Fallible<()> { replica.sync(server)?; - println!("sync complete."); + write!(w, "sync complete.\n")?; Ok(()) } #[cfg(test)] mod test { use super::*; - use crate::invocation::cmd::test::{test_replica, test_server}; + use crate::invocation::cmd::test::*; use tempdir::TempDir; #[test] fn test_add() { + let mut w = test_writer(); let mut replica = test_replica(); let server_dir = TempDir::new("test").unwrap(); let mut server = test_server(&server_dir); - // this just has to not fail -- the details of the actual sync are - // tested thoroughly in the taskchampion crate - execute(&mut replica, &mut server).unwrap(); + // Note that the details of the actual sync are tested thoroughly in the taskchampion crate + execute(&mut w, &mut replica, &mut server).unwrap(); + assert_eq!(&w.into_string(), "sync complete.\n") } } diff --git a/cli/src/invocation/cmd/test.rs b/cli/src/invocation/cmd/test.rs index 1ff17fd68..8f32723b0 100644 --- a/cli/src/invocation/cmd/test.rs +++ b/cli/src/invocation/cmd/test.rs @@ -1,3 +1,4 @@ +use std::io; use taskchampion::{server, taskstorage, Replica, ServerConfig}; use tempdir::TempDir; @@ -12,3 +13,38 @@ pub(super) fn test_server(dir: &TempDir) -> Box { }) .unwrap() } + +pub(super) struct TestWriter { + data: Vec, +} + +impl TestWriter { + pub(super) fn into_string(self) -> String { + String::from_utf8(self.data).unwrap() + } +} + +impl io::Write for TestWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.data.write(buf) + } + fn flush(&mut self) -> io::Result<()> { + self.data.flush() + } +} + +impl termcolor::WriteColor for TestWriter { + fn supports_color(&self) -> bool { + false + } + fn set_color(&mut self, _spec: &termcolor::ColorSpec) -> io::Result<()> { + Ok(()) + } + fn reset(&mut self) -> io::Result<()> { + Ok(()) + } +} + +pub(super) fn test_writer() -> TestWriter { + TestWriter { data: vec![] } +} diff --git a/cli/src/invocation/cmd/version.rs b/cli/src/invocation/cmd/version.rs index ace90b2ea..8a74a7681 100644 --- a/cli/src/invocation/cmd/version.rs +++ b/cli/src/invocation/cmd/version.rs @@ -1,16 +1,23 @@ use failure::Fallible; +use termcolor::{ColorSpec, WriteColor}; -pub(crate) fn execute() -> Fallible<()> { - println!("TaskChampion {}", env!("CARGO_PKG_VERSION")); +pub(crate) fn execute(w: &mut W) -> Fallible<()> { + write!(w, "TaskChampion ")?; + w.set_color(ColorSpec::new().set_bold(true))?; + write!(w, "{}\n", env!("CARGO_PKG_VERSION"))?; + w.reset()?; Ok(()) } #[cfg(test)] mod test { use super::*; + use crate::invocation::cmd::test::*; #[test] fn test_version() { - execute().unwrap(); + let mut w = test_writer(); + execute(&mut w).unwrap(); + assert!(w.into_string().starts_with("TaskChampion ")); } } diff --git a/cli/src/invocation/mod.rs b/cli/src/invocation/mod.rs index 42818c142..ec04b3ac1 100644 --- a/cli/src/invocation/mod.rs +++ b/cli/src/invocation/mod.rs @@ -4,6 +4,7 @@ use crate::argparse::{Command, Subcommand}; use config::Config; use failure::Fallible; use taskchampion::{server, Replica, ReplicaConfig, ServerConfig, Uuid}; +use termcolor::{ColorChoice, StandardStream}; mod cmd; mod filter; @@ -17,6 +18,8 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> { log::debug!("command: {:?}", command); log::debug!("settings: {:?}", settings); + let mut w = StandardStream::stdout(ColorChoice::Auto); + // This function examines the command and breaks out the necessary bits to call one of the // `execute` functions in a submodule of `cmd`. @@ -26,11 +29,11 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> { Command { subcommand: Subcommand::Help { summary }, command_name, - } => return cmd::help::execute(command_name, summary), + } => return cmd::help::execute(&mut w, command_name, summary), Command { subcommand: Subcommand::Version, .. - } => return cmd::version::execute(), + } => return cmd::version::execute(&mut w), _ => {} }; @@ -39,7 +42,7 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> { Command { subcommand: Subcommand::Add { modification }, .. - } => return cmd::add::execute(&mut replica, modification), + } => return cmd::add::execute(&mut w, &mut replica, modification), Command { subcommand: @@ -48,29 +51,29 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> { modification, }, .. - } => return cmd::modify::execute(&mut replica, filter, modification), + } => return cmd::modify::execute(&mut w, &mut replica, filter, modification), Command { subcommand: Subcommand::List { report }, .. - } => return cmd::list::execute(&mut replica, report), + } => return cmd::list::execute(&mut w, &mut replica, report), Command { subcommand: Subcommand::Info { filter, debug }, .. - } => return cmd::info::execute(&mut replica, filter, debug), + } => return cmd::info::execute(&mut w, &mut replica, filter, debug), Command { subcommand: Subcommand::Gc, .. - } => return cmd::gc::execute(&mut replica), + } => return cmd::gc::execute(&mut w, &mut replica), Command { subcommand: Subcommand::Sync, .. } => { let mut server = get_server(&settings)?; - return cmd::sync::execute(&mut replica, &mut server); + return cmd::sync::execute(&mut w, &mut replica, &mut server); } // handled in the first match, but here to ensure this match is exhaustive diff --git a/cli/src/invocation/modify.rs b/cli/src/invocation/modify.rs index a1fa7c70b..29802dbab 100644 --- a/cli/src/invocation/modify.rs +++ b/cli/src/invocation/modify.rs @@ -1,9 +1,14 @@ use crate::argparse::{DescriptionMod, Modification}; use failure::Fallible; use taskchampion::TaskMut; +use termcolor::WriteColor; /// Apply the given modification -pub(super) fn apply_modification(task: &mut TaskMut, modification: &Modification) -> Fallible<()> { +pub(super) fn apply_modification( + w: &mut W, + task: &mut TaskMut, + modification: &Modification, +) -> Fallible<()> { match modification.description { DescriptionMod::Set(ref description) => task.set_description(description.clone())?, DescriptionMod::Prepend(ref description) => { @@ -27,7 +32,7 @@ pub(super) fn apply_modification(task: &mut TaskMut, modification: &Modification task.stop()?; } - println!("modified task {}", task.get_uuid()); + write!(w, "modified task {}\n", task.get_uuid())?; Ok(()) }