diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index ced27219..509d5d24 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -29,7 +29,10 @@ jobs: run: cargo build --verbose - name: Test run: cargo test --verbose - - name: Lint + - name: Clippy run: cargo clippy - name: Format run: cargo fmt -- --check + - name: Lint + if: matrix.os != 'windows-latest' + run: "! grep --color -REn 'FIXME|TODO|XXX' src" diff --git a/Cargo.lock b/Cargo.lock index f9b961b0..19dbbc18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,14 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "atty" version = "0.2.14" @@ -63,6 +71,18 @@ name = "doc-comment" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -89,6 +109,14 @@ dependencies = [ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "idna" version = "0.2.0" @@ -103,6 +131,9 @@ dependencies = [ name = "imdl" version = "0.0.0" dependencies = [ + "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "md5 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -128,6 +159,14 @@ name = "libc" version = "0.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "matches" version = "0.1.8" @@ -185,6 +224,11 @@ dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "quote" version = "1.0.2" @@ -274,7 +318,7 @@ name = "same-file" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -413,6 +457,14 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -482,7 +534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -506,7 +558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "winapi-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -520,18 +572,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" +"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" +"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum md5 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" @@ -540,6 +596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum proc-macro-error 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "53c98547ceaea14eeb26fcadf51dc70d01a2479a7839170eae133721105e4428" "checksum proc-macro-error-attr 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c2bf5d493cf5d3e296beccfd61794e445e830dfc8070a9c248ad3ee071392c6c" "checksum proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc" +"checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" "checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" @@ -567,6 +624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8" "checksum syn-mid 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9fd3937748a7eccff61ba5b90af1a20dbf610858923a9192ea0ecb0cb77db1d0" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" @@ -580,5 +638,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" +"checksum winapi-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index b30477c4..24fb81a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,18 +12,21 @@ repository = "https://github.com/casey/intermodal" edition = "2018" [dependencies] -libc = "0.2" -md5 = "0.7" -regex = "1" -serde_bencode = "0.2" -serde_bytes = "0.11" -sha1 = "0.6" -snafu = "0.6" -structopt = "0.3" -tempfile = "3" -url = "2" -walkdir = "2" -static_assertions = "1.1.0" +ansi_term = "0.12" +atty = "0.2" +env_logger = "0.7" +libc = "0.2" +md5 = "0.7" +regex = "1" +serde_bencode = "0.2" +serde_bytes = "0.11" +sha1 = "0.6" +snafu = "0.6" +static_assertions = "1" +structopt = "0.3" +tempfile = "3" +url = "2" +walkdir = "2" [dependencies.serde] version = "1" diff --git a/README.md b/README.md index b6c047f6..7300261a 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ # intermodal: a 40' shipping container for the Internet + +## Colored Output + +Intermodal features colored help, error, and informational output. Colored +output is disabled if Intermodal detects that it is not printing to a TTY. + +To disable colored output, set the `NO_COLOR` environment variable to any +valu or pass `--use-color never` on the command line. + +To force colored output, pass `--use-color always` on the command line. + +## Semantic Versioning and Unstable Features + +Intermodal follows [semantic versioning](https://semver.org/). + +In particular: + +- v0.0.X: Breaking changes may be introduced at any time. +- v0.X.Y: Breaking changes may only be introduced with a minor version number + bump. +- vX.Y.Z: Breaking changes may only be introduced with a major version number + bump + +To avoid premature stabilization and excessive version churn, unstable features +are unavailable unless the `--unstable` / `-u` flag is passed. Unstable +features may be changed or removed at any time. + +``` +$ imdl torrent stats --input tmp +error: Feature `torrent stats subcommand` cannot be used without passing the `--unstable` flag +$ imdl --unstable torrent stats tmp +Torrents processed: 0 +Read failed: 0 +Decode failed: 0 +``` diff --git a/src/common.rs b/src/common.rs index 3e0482ba..8d769e3f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,7 +3,7 @@ pub(crate) use std::{ borrow::Cow, cmp::{Ordering, Reverse}, collections::{BTreeMap, HashMap}, - convert::TryInto, + convert::{Infallible, TryInto}, env, ffi::{OsStr, OsString}, fmt::{self, Display, Formatter}, @@ -11,7 +11,8 @@ pub(crate) use std::{ hash::Hash, io::{self, Read, Write}, path::{Path, PathBuf}, - process, str, + process, + str::{self, FromStr}, time::{SystemTime, SystemTimeError}, usize, }; @@ -28,7 +29,7 @@ pub(crate) use url::Url; pub(crate) use walkdir::WalkDir; // modules -pub(crate) use crate::{bencode, consts, error, torrent}; +pub(crate) use crate::{bencode, consts, error, torrent, use_color}; // traits pub(crate) use crate::{ @@ -38,7 +39,8 @@ pub(crate) use crate::{ // structs and enums pub(crate) use crate::{ env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info, metainfo::Metainfo, - mode::Mode, opt::Opt, subcommand::Subcommand, torrent::Torrent, + mode::Mode, opt::Opt, style::Style, subcommand::Subcommand, torrent::Torrent, + use_color::UseColor, }; // test modules diff --git a/src/env.rs b/src/env.rs index 32dc0b49..bbacbb5e 100644 --- a/src/env.rs +++ b/src/env.rs @@ -4,7 +4,8 @@ pub(crate) struct Env { args: Vec, dir: Box>, pub(crate) err: Box, - pub(crate) _out: Box, + pub(crate) out: Box, + err_style: Style, } impl Env { @@ -14,14 +15,42 @@ impl Env { Err(error) => panic!("Failed to get current directory: {}", error), }; - Self::new(dir, io::stdout(), io::stderr(), env::args()) + 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) + { + Style::inactive() + } else { + Style::active() + }; + + Self::new(dir, io::stdout(), io::stderr(), err_style, env::args()) } pub(crate) fn run(&mut self) -> Result<(), Error> { - Opt::from_iter_safe(&self.args)?.run(self) + #[cfg(windows)] + ansi_term::enable_ansi_support().ok(); + + #[cfg(not(test))] + env_logger::Builder::from_env( + env_logger::Env::new() + .filter("JUST_LOG") + .write_style("JUST_LOG_STYLE"), + ) + .init(); + + let opt = Opt::from_iter_safe(&self.args)?; + + match opt.use_color { + UseColor::Always => self.err_style = Style::active(), + UseColor::Auto => {} + UseColor::Never => self.err_style = Style::inactive(), + } + + opt.run(self) } - pub(crate) fn new(dir: D, out: O, err: E, args: I) -> Self + pub(crate) fn new(dir: D, out: O, err: E, err_style: Style, args: I) -> Self where D: AsRef + 'static, O: Write + 'static, @@ -33,21 +62,35 @@ impl Env { args: args.into_iter().map(Into::into).collect(), dir: Box::new(dir), err: Box::new(err), - _out: Box::new(out), + out: Box::new(out), + err_style, } } pub(crate) fn status(&mut self) -> Result<(), i32> { use structopt::clap::ErrorKind; + if let Err(error) = self.run() { if let Error::Clap { source } = error { - write!(&mut self.err, "{}", source).ok(); + if source.use_stderr() { + write!(&mut self.err, "{}", source).ok(); + } else { + write!(&mut self.out, "{}", source).ok(); + } match source.kind { ErrorKind::VersionDisplayed | ErrorKind::HelpDisplayed => Ok(()), _ => Err(EXIT_FAILURE), } } else { - write!(&mut self.err, "error: {}", error).ok(); + writeln!( + &mut self.err, + "{}{}: {}{}", + self.err_style.error().paint("error"), + self.err_style.message().prefix(), + error, + self.err_style.message().suffix(), + ) + .ok(); Err(EXIT_FAILURE) } } else { diff --git a/src/main.rs b/src/main.rs index f68adf9d..79f3f609 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ clippy::option_unwrap_used, clippy::result_expect_used, clippy::result_unwrap_used, + clippy::unreachable, clippy::wildcard_enum_match_arm )] @@ -53,8 +54,10 @@ mod mode; mod opt; mod path_ext; mod reckoner; +mod style; mod subcommand; mod torrent; +mod use_color; fn main() { if let Err(code) = Env::main().status() { diff --git a/src/opt.rs b/src/opt.rs index 5ba080d2..ac95b77b 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -1,18 +1,25 @@ use crate::common::*; -use structopt::clap::AppSettings; +use structopt::clap::{AppSettings, ArgSettings}; #[derive(StructOpt)] #[structopt( about(consts::ABOUT), version(consts::VERSION), author(consts::AUTHOR), - setting(AppSettings::ColoredHelp), - setting(AppSettings::ColorAuto) + global_setting(AppSettings::ColoredHelp), + global_setting(AppSettings::ColorAuto) )] pub(crate) struct Opt { #[structopt(long = "unstable", short = "u")] unstable: bool, + #[structopt( + long = "color", + default_value = use_color::AUTO, + set = ArgSettings::CaseInsensitive, + possible_values = use_color::VALUES, + )] + pub(crate) use_color: UseColor, #[structopt(subcommand)] subcommand: Subcommand, } diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 00000000..fb6d84ce --- /dev/null +++ b/src/style.rs @@ -0,0 +1,30 @@ +#[derive(Clone, Copy, Debug)] +pub(crate) struct Style { + active: bool, +} + +impl Style { + pub(crate) fn active() -> Self { + Self { active: true } + } + + pub(crate) fn inactive() -> Self { + Self { active: false } + } + + pub(crate) fn message(self) -> ansi_term::Style { + if self.active { + ansi_term::Style::new().bold() + } else { + ansi_term::Style::new() + } + } + + pub(crate) fn error(self) -> ansi_term::Style { + if self.active { + ansi_term::Style::new().fg(ansi_term::Color::Red).bold() + } else { + ansi_term::Style::new() + } + } +} diff --git a/src/test_env.rs b/src/test_env.rs index 2cd3ac2b..7832f056 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -15,6 +15,7 @@ impl TestEnv { tempfile::tempdir().unwrap(), out.clone(), err.clone(), + Style::inactive(), iter::once(String::from("imdl")).chain(iter.into_iter().map(|item| item.into())), ); diff --git a/src/torrent/stats.rs b/src/torrent/stats.rs index 3b7ebc0c..6b2b64b5 100644 --- a/src/torrent/stats.rs +++ b/src/torrent/stats.rs @@ -45,17 +45,19 @@ impl Stats { let max = paths.iter().map(|(_, count)| *count).max().unwrap_or(0); let width = max.to_string().len(); - errln!(env, "Keys:"); - for (key, count) in &paths { - if key.starts_with("info/files") { - continue; - } - errln!(env, "{: Result { + match text.to_lowercase().as_str() { + AUTO => Ok(Self::Auto), + ALWAYS => Ok(Self::Always), + NEVER => Ok(Self::Never), + _ => unreachable!(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_str() { + assert_eq!(UseColor::Auto, AUTO.parse().unwrap()); + assert_eq!(UseColor::Always, ALWAYS.parse().unwrap()); + assert_eq!(UseColor::Never, NEVER.parse().unwrap()); + assert_eq!(UseColor::Auto, AUTO.to_uppercase().parse().unwrap()); + assert_eq!(UseColor::Always, ALWAYS.to_uppercase().parse().unwrap()); + assert_eq!(UseColor::Never, NEVER.to_uppercase().parse().unwrap()); + } +}