diff --git a/src/common.rs b/src/common.rs index 003bbd3..a547e17 100644 --- a/src/common.rs +++ b/src/common.rs @@ -56,7 +56,6 @@ pub(crate) use crate::{ pub(crate) use std::{ cell::RefCell, io::Cursor, - iter, ops::{Deref, DerefMut}, rc::Rc, time::{Duration, Instant}, @@ -68,4 +67,4 @@ pub(crate) use crate::testing; // test structs and enums #[cfg(test)] -pub(crate) use crate::{capture::Capture, test_env::TestEnv}; +pub(crate) use crate::{capture::Capture, test_env::TestEnv, test_env_builder::TestEnvBuilder}; diff --git a/src/env.rs b/src/env.rs index bea3d36..3fe7659 100644 --- a/src/env.rs +++ b/src/env.rs @@ -6,6 +6,8 @@ pub(crate) struct Env { pub(crate) err: Box, pub(crate) out: Box, err_style: Style, + out_style: Style, + out_is_term: bool, } impl Env { @@ -15,16 +17,32 @@ impl Env { Err(error) => panic!("Failed to get current directory: {}", error), }; - let err_style = if env::var_os("NO_COLOR").is_some() - || env::var_os("TERM").as_deref() == Some(OsStr::new("dumb")) - || !atty::is(atty::Stream::Stderr) - { + let no_color = env::var_os("NO_COLOR").is_some() + || env::var_os("TERM").as_deref() == Some(OsStr::new("dumb")); + + let err_style = if no_color || !atty::is(atty::Stream::Stderr) { Style::inactive() } else { Style::active() }; - Self::new(dir, io::stdout(), io::stderr(), err_style, env::args()) + let out_style = if no_color || !atty::is(atty::Stream::Stdout) { + Style::inactive() + } else { + Style::active() + }; + + let out_is_term = atty::is(atty::Stream::Stdout); + + Self::new( + dir, + io::stdout(), + out_style, + out_is_term, + io::stderr(), + err_style, + env::args(), + ) } pub(crate) fn run(&mut self) -> Result<(), Error> { @@ -50,7 +68,15 @@ impl Env { opt.run(self) } - pub(crate) fn new(dir: D, out: O, err: E, err_style: Style, args: I) -> Self + pub(crate) fn new( + dir: D, + out: O, + out_style: Style, + out_is_term: bool, + err: E, + err_style: Style, + args: I, + ) -> Self where D: AsRef + 'static, O: Write + 'static, @@ -63,6 +89,8 @@ impl Env { dir: Box::new(dir), err: Box::new(err), out: Box::new(out), + out_style, + out_is_term, err_style, } } @@ -116,6 +144,14 @@ impl Env { pub(crate) fn resolve(&self, path: impl AsRef) -> PathBuf { self.dir().join(path).clean() } + + pub(crate) fn out_is_term(&self) -> bool { + self.out_is_term + } + + pub(crate) fn out_style(&self) -> Style { + self.out_style + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index c54c8ff..326e440 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,9 @@ mod testing; #[cfg(test)] mod test_env; +#[cfg(test)] +mod test_env_builder; + #[cfg(test)] mod capture; diff --git a/src/metainfo.rs b/src/metainfo.rs index c137a37..ca87bab 100644 --- a/src/metainfo.rs +++ b/src/metainfo.rs @@ -21,6 +21,7 @@ impl Metainfo { Self::deserialize(path, &bytes) } + #[cfg(test)] pub(crate) fn dump(&self, path: impl AsRef) -> Result<(), Error> { let path = path.as_ref(); let bytes = serde_bencode::ser::to_bytes(&self).context(error::MetainfoSerialize)?; diff --git a/src/style.rs b/src/style.rs index fb6d84c..d4e00dc 100644 --- a/src/style.rs +++ b/src/style.rs @@ -27,4 +27,12 @@ impl Style { ansi_term::Style::new() } } + + pub(crate) fn blue(self) -> ansi_term::Style { + if self.active { + ansi_term::Style::new().fg(ansi_term::Color::Blue) + } else { + ansi_term::Style::new() + } + } } diff --git a/src/table.rs b/src/table.rs index 44fb9eb..6cdf14f 100644 --- a/src/table.rs +++ b/src/table.rs @@ -13,6 +13,10 @@ impl Table { self.rows.push((name, Value::Scalar(value.to_string()))); } + pub(crate) fn size(&mut self, name: &'static str, bytes: Bytes) { + self.rows.push((name, Value::Size(bytes))); + } + pub(crate) fn tiers( &mut self, name: &'static str, @@ -47,7 +51,7 @@ impl Table { .unwrap_or(0) } - pub(crate) fn write_human_readable(&self, out: &mut dyn Write) -> io::Result<()> { + pub(crate) fn write_human_readable(&self, out: &mut dyn Write, style: Style) -> io::Result<()> { fn padding(out: &mut dyn Write, n: usize) -> io::Result<()> { write!(out, "{:width$}", "", width = n) } @@ -55,10 +59,17 @@ impl Table { let name_width = self.name_width(); for (name, value) in self.rows() { - write!(out, "{:>width$}", name, width = name_width)?; + write!( + out, + "{:width$}{}", + "", + style.blue().paint(*name), + width = name_width - UnicodeWidthStr::width(*name), + )?; match value { - Value::Scalar(value) => writeln!(out, " {}", value)?, + Value::Scalar(scalar) => writeln!(out, " {}", scalar)?, + Value::Size(bytes) => writeln!(out, " {}", bytes)?, Value::Tiers(tiers) => { let tier_name_width = tiers .iter() @@ -92,31 +103,81 @@ impl Table { Ok(()) } + + pub(crate) fn write_tab_delimited(&self, out: &mut dyn Write) -> io::Result<()> { + for (name, value) in self.rows() { + write!(out, "{}\t", name)?; + match value { + Value::Scalar(scalar) => writeln!(out, "{}", scalar)?, + Value::Size(Bytes(value)) => writeln!(out, "{}", value)?, + Value::Tiers(tiers) => { + for (i, value) in tiers.iter().flat_map(|(_name, values)| values).enumerate() { + if i > 0 { + write!(out, "\t")?; + } + write!(out, "{}", value)?; + } + writeln!(out)?; + } + } + } + + Ok(()) + } } enum Value { Scalar(String), Tiers(Vec<(String, Vec)>), + Size(Bytes), } #[cfg(test)] mod tests { use super::*; - fn human_readable(table: Table, want: &str) { + fn human_readable(table: &Table, want: &str) { let mut cursor = Cursor::new(Vec::new()); - table.write_human_readable(&mut cursor).unwrap(); + table + .write_human_readable(&mut cursor, Style::inactive()) + .unwrap(); + let have = String::from_utf8(cursor.into_inner()).unwrap(); + if have != want { + panic!("have != want:\nHAVE:\n{}\nWANT:\n{}", have, want); + } + } + + fn tab_delimited(table: &Table, want: &str) { + let mut cursor = Cursor::new(Vec::new()); + table.write_tab_delimited(&mut cursor).unwrap(); let have = String::from_utf8(cursor.into_inner()).unwrap(); if have != want { panic!("have != want:\nHAVE:\n{}\nWANT:\n{}", have, want); } } + #[test] + fn color() { + let mut table = Table::new(); + table.row("Here", "bar"); + table.row("There", "baz"); + let mut cursor = Cursor::new(Vec::new()); + table + .write_human_readable(&mut cursor, Style::active()) + .unwrap(); + let have = String::from_utf8(cursor.into_inner()).unwrap(); + assert_eq!( + have, + " \u{1b}[34mHere\u{1b}[0m bar\n\u{1b}[34mThere\u{1b}[0m baz\n" + ); + } + #[test] fn single_row() { let mut table = Table::new(); table.row("Foo", "bar"); - human_readable(table, "Foo bar\n"); + human_readable(&table, "Foo bar\n"); + tab_delimited(&table, "Foo\tbar\n"); } #[test] @@ -124,7 +185,8 @@ mod tests { let mut table = Table::new(); table.row("Foo", "bar"); table.row("X", "y"); - human_readable(table, "Foo bar\n X y\n"); + human_readable(&table, "Foo bar\n X y\n"); + tab_delimited(&table, "Foo\tbar\nX\ty\n"); } #[test] @@ -132,7 +194,7 @@ mod tests { let mut table = Table::new(); table.tiers("Foo", vec![("Bar", &["a", "b"]), ("Baz", &["x", "y"])]); human_readable( - table, + &table, "\ Foo Bar: a b @@ -140,6 +202,7 @@ Foo Bar: a y ", ); + tab_delimited(&table, "Foo\ta\tb\tx\ty\n"); } #[test] @@ -157,7 +220,7 @@ Foo Bar: a ], ); human_readable( - table, + &table, " First Some: the thing Other: about @@ -168,5 +231,9 @@ Second Row: the that ", ); + tab_delimited( + &table, + "First\tthe\tthing\tabout\tthat\nSecond\tthe\tthing\tabout\tthat\n", + ); } } diff --git a/src/test_env.rs b/src/test_env.rs index 7832f05..5419b39 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -7,18 +7,7 @@ pub(crate) struct TestEnv { } impl TestEnv { - pub(crate) fn new(iter: impl IntoIterator>) -> Self { - let err = Capture::new(); - let out = Capture::new(); - - let env = Env::new( - tempfile::tempdir().unwrap(), - out.clone(), - err.clone(), - Style::inactive(), - iter::once(String::from("imdl")).chain(iter.into_iter().map(|item| item.into())), - ); - + pub(crate) fn new(env: Env, err: Capture, out: Capture) -> TestEnv { Self { err, env, out } } diff --git a/src/test_env_builder.rs b/src/test_env_builder.rs new file mode 100644 index 0000000..b359cb3 --- /dev/null +++ b/src/test_env_builder.rs @@ -0,0 +1,62 @@ +use crate::common::*; + +pub(crate) struct TestEnvBuilder { + args: Vec, + out_is_term: bool, + use_color: bool, +} + +impl TestEnvBuilder { + pub(crate) fn new() -> TestEnvBuilder { + TestEnvBuilder { + args: Vec::new(), + out_is_term: false, + use_color: false, + } + } + + pub(crate) fn out_is_term(mut self) -> Self { + self.out_is_term = true; + self + } + + pub(crate) fn arg(mut self, arg: impl Into) -> Self { + self.args.push(arg.into()); + self + } + + pub(crate) fn args(mut self, args: impl IntoIterator>) -> Self { + for arg in args { + self.args.push(arg.into()); + } + self + } + + pub(crate) fn arg_slice(mut self, args: &[&str]) -> Self { + for arg in args.iter().cloned() { + self.args.push(arg.to_owned()); + } + self + } + + pub(crate) fn build(self) -> TestEnv { + let err = Capture::new(); + let out = Capture::new(); + + let env = Env::new( + tempfile::tempdir().unwrap(), + out.clone(), + if self.use_color && self.out_is_term { + Style::active() + } else { + Style::inactive() + }, + self.out_is_term, + err.clone(), + Style::inactive(), + self.args, + ); + + TestEnv::new(env, err, out) + } +} diff --git a/src/testing.rs b/src/testing.rs index 05ebaec..133991c 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,5 +1,5 @@ use crate::common::*; pub(crate) fn env(iter: impl IntoIterator>) -> TestEnv { - TestEnv::new(iter) + TestEnvBuilder::new().arg("imdl").args(iter).build() } diff --git a/src/torrent/create.rs b/src/torrent/create.rs index f33007e..9c3c0ff 100644 --- a/src/torrent/create.rs +++ b/src/torrent/create.rs @@ -872,13 +872,20 @@ mod tests { #[test] fn output() { - let mut env = environment(&[ - "--input", - "foo", - "--announce", - "http://bar", - "--no-creation-date", - ]); + let mut env = TestEnvBuilder::new() + .arg_slice(&[ + "imdl", + "torrent", + "create", + "--input", + "foo", + "--announce", + "http://bar", + "--no-creation-date", + ]) + .out_is_term() + .build(); + let dir = env.resolve("foo"); fs::create_dir(&dir).unwrap(); fs::write(dir.join("a"), "abc").unwrap(); diff --git a/src/torrent/show.rs b/src/torrent/show.rs index 5118714..9054ea3 100644 --- a/src/torrent/show.rs +++ b/src/torrent/show.rs @@ -33,12 +33,6 @@ mod tests { #[test] fn output() { - let mut env = testing::env( - ["torrent", "show", "--input", "foo.torrent"] - .iter() - .cloned(), - ); - let metainfo = Metainfo { announce: "announce".into(), announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]), @@ -61,14 +55,20 @@ mod tests { }, }; - let path = env.resolve("foo.torrent"); + { + let mut env = TestEnvBuilder::new() + .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) + .out_is_term() + .build(); - metainfo.dump(path).unwrap(); + let path = env.resolve("foo.torrent"); - env.run().unwrap(); + metainfo.dump(path).unwrap(); - let have = env.out(); - let want = " Name foo + env.run().unwrap(); + + let have = env.out(); + let want = " Name foo Comment comment Created 1970-01-01 00:00:01 UTC Source source @@ -76,8 +76,7 @@ mod tests { Torrent Size 252 bytes Content Size 20 bytes Private yes - Trackers Main: announce - Tier 1: announce + Trackers Tier 1: announce b Tier 2: c Piece Size 16 KiB @@ -85,6 +84,37 @@ Content Size 20 bytes File Count 1 "; - assert_eq!(have, want); + assert_eq!(have, want); + } + + { + let mut env = TestEnvBuilder::new() + .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"]) + .build(); + + let path = env.resolve("foo.torrent"); + + metainfo.dump(path).unwrap(); + + env.run().unwrap(); + + let have = env.out(); + let want = "\ +Name\tfoo +Comment\tcomment +Created\t1970-01-01 00:00:01 UTC +Source\tsource +Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc +Torrent Size\t252 +Content Size\t20 +Private\tyes +Trackers\tannounce\tb\tc +Piece Size\t16384 +Piece Count\t1 +File Count\t1 +"; + + assert_eq!(have, want); + } } } diff --git a/src/torrent_summary.rs b/src/torrent_summary.rs index b13000d..5dbe1d1 100644 --- a/src/torrent_summary.rs +++ b/src/torrent_summary.rs @@ -45,9 +45,16 @@ impl TorrentSummary { pub(crate) fn write(&self, env: &mut Env) -> Result<(), Error> { let table = self.table(); - table - .write_human_readable(&mut env.out) - .context(error::Stdout)?; + if env.out_is_term() { + let out_style = env.out_style(); + table + .write_human_readable(&mut env.out, out_style) + .context(error::Stdout)?; + } else { + table + .write_tab_delimited(&mut env.out) + .context(error::Stdout)?; + } Ok(()) } @@ -81,9 +88,9 @@ impl TorrentSummary { table.row("Info Hash", self.infohash); - table.row("Torrent Size", self.size); + table.size("Torrent Size", self.size); - table.row("Content Size", self.metainfo.info.mode.total_size()); + table.size("Content Size", self.metainfo.info.mode.total_size()); table.row( "Private", @@ -97,7 +104,13 @@ impl TorrentSummary { match &self.metainfo.announce_list { Some(tiers) => { let mut value = Vec::new(); - value.push(("Main".to_owned(), vec![self.metainfo.announce.clone()])); + + if !tiers + .iter() + .any(|tier| tier.contains(&self.metainfo.announce)) + { + value.push(("Main".to_owned(), vec![self.metainfo.announce.clone()])); + } for (i, tier) in tiers.iter().enumerate() { value.push((format!("Tier {}", i + 1), tier.clone())); @@ -108,7 +121,7 @@ impl TorrentSummary { None => table.row("Tracker", &self.metainfo.announce), } - table.row("Piece Size", Bytes::from(self.metainfo.info.piece_length)); + table.size("Piece Size", Bytes::from(self.metainfo.info.piece_length)); table.row("Piece Count", self.metainfo.info.pieces.len() / 20);