From 192445d7e4a134a044e714e606ce0b711b2da810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 24 Oct 2021 00:17:12 +0200 Subject: [PATCH] make use of termcolor crate to disable colors (#236) * table: replace custom color by termcolor * table: deactivate colors if not tty * table: rename printable to print, add more comments * table: make use of writters, fix tests * doc: update changelog * doc: add page to wiki --- CHANGELOG.md | 5 + Cargo.lock | 1 + Cargo.toml | 1 + src/domain/mbox/mbox_handler.rs | 5 +- src/domain/mbox/mboxes_entity.rs | 16 +- src/domain/msg/envelopes_entity.rs | 14 +- src/output/mod.rs | 3 + src/output/output_service.rs | 21 +- src/output/print.rs | 28 ++ src/ui/table.rs | 454 +++++++++++++++-------------- wiki | 2 +- 11 files changed, 303 insertions(+), 247 deletions(-) create mode 100644 src/output/print.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 78cfd2e..c3bdd0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Disable color support [#185] + ### Fixed - Error when receiving notification from `notify` command [#228] @@ -334,6 +338,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#160]: https://github.com/soywod/himalaya/issues/160 [#162]: https://github.com/soywod/himalaya/issues/162 [#176]: https://github.com/soywod/himalaya/issues/176 +[#185]: https://github.com/soywod/himalaya/issues/185 [#186]: https://github.com/soywod/himalaya/issues/186 [#190]: https://github.com/soywod/himalaya/issues/190 [#193]: https://github.com/soywod/himalaya/issues/193 diff --git a/Cargo.lock b/Cargo.lock index 314ee92..8916b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,7 @@ dependencies = [ "serde", "serde_json", "shellexpand", + "termcolor", "terminal_size", "toml", "tree_magic", diff --git a/Cargo.toml b/Cargo.toml index 9612fe2..6207020 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ rfc2047-decoder = "0.1.2" serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.61" shellexpand = "2.1.0" +termcolor = "1.1" terminal_size = "0.1.15" toml = "0.5.8" tree_magic = "0.2.3" diff --git a/src/domain/mbox/mbox_handler.rs b/src/domain/mbox/mbox_handler.rs index 86209a7..0111949 100644 --- a/src/domain/mbox/mbox_handler.rs +++ b/src/domain/mbox/mbox_handler.rs @@ -20,13 +20,12 @@ pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceI #[cfg(test)] mod tests { use serde::Serialize; - use std::fmt::Display; use super::*; use crate::{ config::Config, domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg}, - output::OutputJson, + output::{OutputJson, Print}, }; #[test] @@ -34,7 +33,7 @@ mod tests { struct OutputServiceTest; impl OutputServiceInterface for OutputServiceTest { - fn print(&self, data: T) -> Result<()> { + fn print(&self, data: T) -> Result<()> { let data = serde_json::to_string(&OutputJson::new(data))?; assert_eq!( data, diff --git a/src/domain/mbox/mboxes_entity.rs b/src/domain/mbox/mboxes_entity.rs index 8139f80..f554d73 100644 --- a/src/domain/mbox/mboxes_entity.rs +++ b/src/domain/mbox/mboxes_entity.rs @@ -2,14 +2,13 @@ //! //! This module contains the definition of the mailboxes and its traits implementations. +use anyhow::Result; use serde::Serialize; -use std::{ - fmt::{self, Display}, - ops::Deref, -}; +use std::ops::Deref; use crate::{ domain::{Mbox, RawMbox}, + output::{Print, WriteWithColor}, ui::Table, }; @@ -29,10 +28,11 @@ impl<'a> Deref for Mboxes<'a> { } } -/// Makes the mailboxes displayable. -impl<'a> Display for Mboxes<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\n{}", Table::render(&self)) +/// Makes the mailboxes printable. +impl<'a> Print for Mboxes<'a> { + fn print(&self, writter: &mut W) -> Result<()> { + writeln!(writter)?; + Table::println(writter, &self) } } diff --git a/src/domain/msg/envelopes_entity.rs b/src/domain/msg/envelopes_entity.rs index 2976916..9cd2600 100644 --- a/src/domain/msg/envelopes_entity.rs +++ b/src/domain/msg/envelopes_entity.rs @@ -1,13 +1,10 @@ use anyhow::{Error, Result}; use serde::Serialize; -use std::{ - convert::TryFrom, - fmt::{self, Display}, - ops::Deref, -}; +use std::{convert::TryFrom, ops::Deref}; use crate::{ domain::{msg::Envelope, RawEnvelope}, + output::{Print, WriteWithColor}, ui::Table, }; @@ -39,8 +36,9 @@ impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> { } } -impl<'a> Display for Envelopes<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\n{}", Table::render(&self)) +impl<'a> Print for Envelopes<'a> { + fn print(&self, writter: &mut W) -> Result<()> { + println!(); + Table::println(writter, &self) } } diff --git a/src/output/mod.rs b/src/output/mod.rs index 201236b..3435281 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -7,3 +7,6 @@ pub use output_utils::*; pub mod output_service; pub use output_service::*; + +pub mod print; +pub use print::*; diff --git a/src/output/output_service.rs b/src/output/output_service.rs index 27229ea..2a908bc 100644 --- a/src/output/output_service.rs +++ b/src/output/output_service.rs @@ -1,10 +1,14 @@ use anyhow::{anyhow, Error, Result}; +use atty::Stream; use log::debug; use serde::Serialize; use std::{ convert::{TryFrom, TryInto}, fmt, }; +use termcolor::{ColorChoice, StandardStream}; + +use crate::output::Print; #[derive(Debug, PartialEq)] pub enum OutputFmt { @@ -58,7 +62,7 @@ impl OutputJson { } pub trait OutputServiceInterface { - fn print(&self, data: T) -> Result<()>; + fn print(&self, data: T) -> Result<()>; fn is_json(&self) -> bool; } @@ -70,10 +74,21 @@ pub struct OutputService { impl OutputServiceInterface for OutputService { /// Print the provided item out according to the formatting setting when you created this /// struct. - fn print(&self, data: T) -> Result<()> { + fn print(&self, data: T) -> Result<()> { match self.fmt { OutputFmt::Plain => { - println!("{}", data) + data.print(&mut 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 + }))?; } OutputFmt::Json => { print!("{}", serde_json::to_string(&OutputJson::new(data))?) diff --git a/src/output/print.rs b/src/output/print.rs new file mode 100644 index 0000000..261c263 --- /dev/null +++ b/src/output/print.rs @@ -0,0 +1,28 @@ +use anyhow::{Context, Result}; +use std::io; +use termcolor::{StandardStream, WriteColor}; + +pub trait WriteWithColor: io::Write + WriteColor {} + +impl WriteWithColor for StandardStream {} + +pub trait Print { + fn print(&self, writter: &mut W) -> Result<()>; + + fn println(&self, writter: &mut W) -> Result<()> { + println!(); + self.print(writter) + } +} + +impl Print for &str { + fn print(&self, writter: &mut W) -> Result<()> { + write!(writter, "{}", self).context(format!(r#"cannot print string "{}""#, self)) + } +} + +impl Print for String { + fn print(&self, writter: &mut W) -> Result<()> { + self.as_str().print(writter) + } +} diff --git a/src/ui/table.rs b/src/ui/table.rs index b3c7a6e..6765ca0 100644 --- a/src/ui/table.rs +++ b/src/ui/table.rs @@ -4,100 +4,66 @@ //! //! [builder design pattern]: https://refactoring.guru/design-patterns/builder +use anyhow::{Context, Result}; use log::trace; -use std::fmt; +use termcolor::{Color, ColorSpec}; use terminal_size; use unicode_width::UnicodeWidthStr; -/// Define the default terminal size. -/// It is used when the size cannot be determined by the `terminal_size` crate. +use crate::output::{Print, WriteWithColor}; + +/// Defines the default terminal size. +/// This is used when the size cannot be determined by the `terminal_size` crate. +/// TODO: make this customizable. pub const DEFAULT_TERM_WIDTH: usize = 80; -/// Define the minimum size of a shrinked cell. +/// Defines the minimum size of a shrinked cell. /// TODO: make this customizable. pub const MAX_SHRINK_WIDTH: usize = 5; -/// Wrapper around [ANSI escape codes] for styling cells. -/// -/// [ANSI escape codes]: https://en.wikipedia.org/wiki/ANSI_escape_code -#[derive(Debug)] -pub struct Style( - /// The style/color code. - u8, - /// The brightness code. - u8, - /// The shade code. - u8, -); - -impl fmt::Display for Style { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Style(color, bright, shade) = self; - let mut style = String::new(); - - // Push first the style/color code. - style.push_str(&color.to_string()); - - // Then push the brightness code if exist. - if *bright > 0 { - style.push_str(";"); - style.push_str(&bright.to_string()); - }; - - // Then push the shade code if exist. - if *shade > 0 { - style.push_str(";"); - style.push_str(&shade.to_string()); - }; - - write!(f, "\x1b[{}m", style) - } -} - -/// Representation of a table cell. -#[derive(Debug)] +/// Represents a cell in a table. +#[derive(Debug, Default)] pub struct Cell { - /// The list of style applied to the cell. - styles: Vec