diff --git a/src/main.rs b/src/main.rs index 38726625..ae19ba19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,7 @@ use url::Url; use himalaya::{ account, compl, config::{self, DeserializedConfig}, - email, flag, folder, man, - output::{self, OutputFmt}, + email, flag, folder, man, output, printer::StdoutPrinter, tpl, }; @@ -51,7 +50,7 @@ fn main() -> Result<()> { let (account_config, backend_config) = config.to_configs(None)?; let mut backend = BackendBuilder::build(&account_config, &backend_config)?; let mut sender = SenderBuilder::build(&account_config)?; - let mut printer = StdoutPrinter::from_fmt(OutputFmt::Plain); + let mut printer = StdoutPrinter::default(); return email::handlers::mailto( &account_config, @@ -108,8 +107,7 @@ fn main() -> Result<()> { // inits services let mut backend = BackendBuilder::build(&account_config, &backend_config)?; let mut sender = SenderBuilder::build(&account_config)?; - let mut printer = - StdoutPrinter::from_opt_str(m.get_one::("output").map(String::as_str))?; + let mut printer = StdoutPrinter::try_from(&m)?; // checks account commands match account::args::matches(&m)? { diff --git a/src/output/args.rs b/src/output/args.rs index 386db719..13bd5173 100644 --- a/src/output/args.rs +++ b/src/output/args.rs @@ -4,10 +4,13 @@ use clap::Arg; +pub(crate) const ARG_OUTPUT: &str = "output"; +pub(crate) const ARG_COLOR: &str = "color"; + /// Output arguments. pub fn args() -> Vec { vec![ - Arg::new("output") + Arg::new(ARG_OUTPUT) .help("Defines the output format") .long("output") .short('o') @@ -22,5 +25,30 @@ pub fn args() -> Vec { .value_name("LEVEL") .value_parser(["error", "warn", "info", "debug", "trace"]) .default_value("info"), + Arg::new(ARG_COLOR) + .help("Controls when to use colors.") + .long_help( + " +This flag controls when to use colors. The default setting is 'auto', which +means himalaya will try to guess when to use colors. For example, if himalaya is +printing to a terminal, then it will use colors, but if it is redirected to a +file or a pipe, then it will suppress color output. himalaya will suppress color +output in some other circumstances as well. For example, if the TERM +environment variable is not set or set to 'dumb', then himalaya will not use +colors. + +The possible values for this flag are: + +never Colors will never be used. +auto The default. himalaya tries to be smart. +always Colors will always be used regardless of where output is sent. +ansi Like 'always', but emits ANSI escapes (even in a Windows console). +", + ) + .long("color") + .short('C') + .value_parser(["never", "auto", "always", "ansi"]) + .default_value("auto") + .value_name("WHEN"), ] } diff --git a/src/output/output.rs b/src/output/output.rs index c0efbf82..9b1d19b3 100644 --- a/src/output/output.rs +++ b/src/output/output.rs @@ -1,31 +1,30 @@ use anyhow::{anyhow, Error, Result}; -use std::{convert::TryFrom, fmt}; +use atty::Stream; +use serde::Serialize; +use std::{fmt, str::FromStr}; +use termcolor::ColorChoice; /// Represents the available output formats. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum OutputFmt { Plain, Json, } -impl From<&str> for OutputFmt { - fn from(fmt: &str) -> Self { - match fmt { - slice if slice.eq_ignore_ascii_case("json") => Self::Json, - _ => Self::Plain, - } +impl Default for OutputFmt { + fn default() -> Self { + Self::Plain } } -impl TryFrom> for OutputFmt { - type Error = Error; +impl FromStr for OutputFmt { + type Err = Error; - fn try_from(fmt: Option<&str>) -> Result { + fn from_str(fmt: &str) -> Result { match fmt { - Some(fmt) if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json), - Some(fmt) if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain), - None => Ok(Self::Plain), - Some(fmt) => Err(anyhow!(r#"cannot parse output format "{}""#, fmt)), + fmt if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json), + fmt if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain), + unknown => Err(anyhow!("cannot parse output format {}", unknown)), } } } @@ -39,3 +38,76 @@ impl fmt::Display for OutputFmt { write!(f, "{}", fmt) } } + +/// Defines a struct-wrapper to provide a JSON output. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct OutputJson { + response: T, +} + +impl OutputJson { + pub fn new(response: T) -> Self { + Self { response } + } +} + +/// Represent the available color configs. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ColorFmt { + Never, + Always, + Ansi, + Auto, +} + +impl Default for ColorFmt { + fn default() -> Self { + Self::Auto + } +} + +impl FromStr for ColorFmt { + type Err = Error; + + fn from_str(fmt: &str) -> Result { + match fmt { + fmt if fmt.eq_ignore_ascii_case("never") => Ok(Self::Never), + fmt if fmt.eq_ignore_ascii_case("always") => Ok(Self::Always), + fmt if fmt.eq_ignore_ascii_case("ansi") => Ok(Self::Ansi), + fmt if fmt.eq_ignore_ascii_case("auto") => Ok(Self::Auto), + unknown => Err(anyhow!("cannot parse color format {}", unknown)), + } + } +} + +impl From for ColorChoice { + fn from(fmt: ColorFmt) -> Self { + match fmt { + ColorFmt::Never => Self::Never, + ColorFmt::Always => Self::Always, + ColorFmt::Ansi => Self::AlwaysAnsi, + ColorFmt::Auto => { + if atty::is(Stream::Stdout) { + // Otherwise let's `termcolor` decide by + // inspecting the environment. From the [doc]: + // + // * If `NO_COLOR` is set to any value, then + // colors will be suppressed. + // + // * If `TERM` is set to dumb, then colors will be + // suppressed. + // + // * In non-Windows environments, if `TERM` is not + // set, then colors will be suppressed. + // + // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection + Self::Auto + } else { + // Colors should be deactivated if the terminal is + // not a tty. + Self::Never + } + } + } + } +} diff --git a/src/printer/printer.rs b/src/printer/printer.rs index 2370db1e..872176db 100644 --- a/src/printer/printer.rs +++ b/src/printer/printer.rs @@ -1,10 +1,10 @@ -use anyhow::{Context, Result}; -use atty::Stream; +use anyhow::{Context, Error, Result}; +use clap::ArgMatches; use std::fmt::{self, Debug}; -use termcolor::{ColorChoice, StandardStream}; +use termcolor::StandardStream; use crate::{ - output::OutputFmt, + output::{args, ColorFmt, OutputFmt}, printer::{Print, PrintTable, PrintTableOpts, WriteColor}, }; @@ -24,29 +24,18 @@ pub struct StdoutPrinter { pub fmt: OutputFmt, } -impl StdoutPrinter { - pub fn from_fmt(fmt: OutputFmt) -> Self { - let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) { - // Colors should be deactivated if the terminal is not a tty. - ColorChoice::Never - } else { - // Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]: - // - If `NO_COLOR` is set to any value, then colors will be suppressed. - // - If `TERM` is set to dumb, then colors will be suppressed. - // - In non-Windows environments, if `TERM` is not set, then colors will be suppressed. - // - // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection - ColorChoice::Auto - }); - let writer = Box::new(writer); - Self { writer, fmt } +impl Default for StdoutPrinter { + fn default() -> Self { + let fmt = OutputFmt::default(); + let writer = Box::new(StandardStream::stdout(ColorFmt::default().into())); + Self { fmt, writer } } +} - pub fn from_opt_str(s: Option<&str>) -> Result { - Ok(Self { - fmt: OutputFmt::try_from(s)?, - ..Self::from_fmt(OutputFmt::Plain) - }) +impl StdoutPrinter { + pub fn new(fmt: OutputFmt, color: ColorFmt) -> Self { + let writer = Box::new(StandardStream::stdout(color.into())); + Self { fmt, writer } } } @@ -86,3 +75,29 @@ impl Printer for StdoutPrinter { self.fmt == OutputFmt::Json } } + +impl From for StdoutPrinter { + fn from(fmt: OutputFmt) -> Self { + Self::new(fmt, ColorFmt::Auto) + } +} + +impl TryFrom<&ArgMatches> for StdoutPrinter { + type Error = Error; + + fn try_from(m: &ArgMatches) -> Result { + let fmt: OutputFmt = m + .get_one::(args::ARG_OUTPUT) + .map(String::as_str) + .unwrap() + .parse()?; + + let color: ColorFmt = m + .get_one::(args::ARG_COLOR) + .map(String::as_str) + .unwrap() + .parse()?; + + Ok(Self::new(fmt, color)) + } +}