Improve torrent display formatting
- Use colors - Use cut-friendly formatting when not writing to terminal - Show sizes as number of bytes when not writing to terminal type: changed
This commit is contained in:
parent
5c5dac1fe5
commit
35a0e8f9b7
|
@ -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};
|
||||
|
|
48
src/env.rs
48
src/env.rs
|
@ -6,6 +6,8 @@ pub(crate) struct Env {
|
|||
pub(crate) err: Box<dyn Write>,
|
||||
pub(crate) out: Box<dyn Write>,
|
||||
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<D, O, E, S, I>(dir: D, out: O, err: E, err_style: Style, args: I) -> Self
|
||||
pub(crate) fn new<D, O, E, S, I>(
|
||||
dir: D,
|
||||
out: O,
|
||||
out_style: Style,
|
||||
out_is_term: bool,
|
||||
err: E,
|
||||
err_style: Style,
|
||||
args: I,
|
||||
) -> Self
|
||||
where
|
||||
D: AsRef<Path> + '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<Path>) -> 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)]
|
||||
|
|
|
@ -42,6 +42,9 @@ mod testing;
|
|||
#[cfg(test)]
|
||||
mod test_env;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_env_builder;
|
||||
|
||||
#[cfg(test)]
|
||||
mod capture;
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ impl Metainfo {
|
|||
Self::deserialize(path, &bytes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn dump(&self, path: impl AsRef<Path>) -> Result<(), Error> {
|
||||
let path = path.as_ref();
|
||||
let bytes = serde_bencode::ser::to_bytes(&self).context(error::MetainfoSerialize)?;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
85
src/table.rs
85
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<String>)>),
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,7 @@ pub(crate) struct TestEnv {
|
|||
}
|
||||
|
||||
impl TestEnv {
|
||||
pub(crate) fn new(iter: impl IntoIterator<Item = impl Into<String>>) -> 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 }
|
||||
}
|
||||
|
||||
|
|
62
src/test_env_builder.rs
Normal file
62
src/test_env_builder.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use crate::common::*;
|
||||
|
||||
pub(crate) struct TestEnvBuilder {
|
||||
args: Vec<String>,
|
||||
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<String>) -> Self {
|
||||
self.args.push(arg.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> 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)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::common::*;
|
||||
|
||||
pub(crate) fn env(iter: impl IntoIterator<Item = impl Into<String>>) -> TestEnv {
|
||||
TestEnv::new(iter)
|
||||
TestEnvBuilder::new().arg("imdl").args(iter).build()
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user