diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index ded40278..5f1d5c8f 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -35,12 +35,19 @@ use std::io::prelude::*; use std::rc::Rc; use std::{fmt, io, mem}; +#[cfg(feature = "color")] +use log::Level; use log::Record; #[cfg(feature = "humantime")] mod humantime; pub(crate) mod writer; +#[cfg(feature = "color")] +mod style; +#[cfg(feature = "color")] +pub use style::{Color, Style, StyledValue}; + #[cfg(feature = "humantime")] pub use self::humantime::Timestamp; pub use self::writer::glob::*; @@ -122,6 +129,62 @@ impl Formatter { } } +#[cfg(feature = "color")] +impl Formatter { + /// Begin a new [`Style`]. + /// + /// # Examples + /// + /// Create a bold, red colored style and use it to print the log level: + /// + /// ``` + /// use std::io::Write; + /// use env_logger::fmt::Color; + /// + /// let mut builder = env_logger::Builder::new(); + /// + /// builder.format(|buf, record| { + /// let mut level_style = buf.style(); + /// + /// level_style.set_color(Color::Red).set_bold(true); + /// + /// writeln!(buf, "{}: {}", + /// level_style.value(record.level()), + /// record.args()) + /// }); + /// ``` + /// + /// [`Style`]: struct.Style.html + pub fn style(&self) -> Style { + Style { + buf: self.buf.clone(), + spec: termcolor::ColorSpec::new(), + } + } + + /// Get the default [`Style`] for the given level. + /// + /// The style can be used to print other values besides the level. + pub fn default_level_style(&self, level: Level) -> Style { + let mut level_style = self.style(); + match level { + Level::Trace => level_style.set_color(Color::Cyan), + Level::Debug => level_style.set_color(Color::Blue), + Level::Info => level_style.set_color(Color::Green), + Level::Warn => level_style.set_color(Color::Yellow), + Level::Error => level_style.set_color(Color::Red).set_bold(true), + }; + level_style + } + + /// Get a printable [`Style`] for the given level. + /// + /// The style can only be used to print the level. + pub fn default_styled_level(&self, level: Level) -> StyledValue<'static, Level> { + self.default_level_style(level).into_value(level) + } +} + impl Write for Formatter { fn write(&mut self, buf: &[u8]) -> io::Result { self.buf.borrow_mut().write(buf) @@ -405,7 +468,7 @@ mod tests { fmt.write(&record).expect("failed to write record"); let buf = buf.borrow(); - String::from_utf8(buf.bytes().to_vec()).expect("failed to read record") + String::from_utf8(buf.as_bytes().to_vec()).expect("failed to read record") } fn write_target(target: &str, fmt: DefaultFormat) -> String { diff --git a/src/fmt/writer/termcolor/extern_impl.rs b/src/fmt/style.rs similarity index 62% rename from src/fmt/writer/termcolor/extern_impl.rs rename to src/fmt/style.rs index 89c38223..dd4463ac 100644 --- a/src/fmt/writer/termcolor/extern_impl.rs +++ b/src/fmt/style.rs @@ -1,190 +1,9 @@ use std::borrow::Cow; use std::cell::RefCell; use std::fmt; -use std::io::{self, Write}; use std::rc::Rc; -use std::sync::Mutex; -use log::Level; -use termcolor::{self, ColorChoice, ColorSpec, WriteColor}; - -use crate::fmt::{Formatter, WritableTarget, WriteStyle}; - -pub(in crate::fmt::writer) mod glob { - pub use super::*; -} - -impl Formatter { - /// Begin a new [`Style`]. - /// - /// # Examples - /// - /// Create a bold, red colored style and use it to print the log level: - /// - /// ``` - /// use std::io::Write; - /// use env_logger::fmt::Color; - /// - /// let mut builder = env_logger::Builder::new(); - /// - /// builder.format(|buf, record| { - /// let mut level_style = buf.style(); - /// - /// level_style.set_color(Color::Red).set_bold(true); - /// - /// writeln!(buf, "{}: {}", - /// level_style.value(record.level()), - /// record.args()) - /// }); - /// ``` - /// - /// [`Style`]: struct.Style.html - pub fn style(&self) -> Style { - Style { - buf: self.buf.clone(), - spec: ColorSpec::new(), - } - } - - /// Get the default [`Style`] for the given level. - /// - /// The style can be used to print other values besides the level. - pub fn default_level_style(&self, level: Level) -> Style { - let mut level_style = self.style(); - match level { - Level::Trace => level_style.set_color(Color::Cyan), - Level::Debug => level_style.set_color(Color::Blue), - Level::Info => level_style.set_color(Color::Green), - Level::Warn => level_style.set_color(Color::Yellow), - Level::Error => level_style.set_color(Color::Red).set_bold(true), - }; - level_style - } - - /// Get a printable [`Style`] for the given level. - /// - /// The style can only be used to print the level. - pub fn default_styled_level(&self, level: Level) -> StyledValue<'static, Level> { - self.default_level_style(level).into_value(level) - } -} - -pub(in crate::fmt::writer) struct BufferWriter { - inner: termcolor::BufferWriter, - uncolored_target: Option, -} - -pub(in crate::fmt) struct Buffer { - inner: termcolor::Buffer, - has_uncolored_target: bool, -} - -impl BufferWriter { - pub(in crate::fmt::writer) fn stderr(is_test: bool, write_style: WriteStyle) -> Self { - BufferWriter { - inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()), - uncolored_target: if is_test { - Some(WritableTarget::Stderr) - } else { - None - }, - } - } - - pub(in crate::fmt::writer) fn stdout(is_test: bool, write_style: WriteStyle) -> Self { - BufferWriter { - inner: termcolor::BufferWriter::stdout(write_style.into_color_choice()), - uncolored_target: if is_test { - Some(WritableTarget::Stdout) - } else { - None - }, - } - } - - pub(in crate::fmt::writer) fn pipe( - write_style: WriteStyle, - pipe: Box>, - ) -> Self { - BufferWriter { - // The inner Buffer is never printed from, but it is still needed to handle coloring and other formatting - inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()), - uncolored_target: Some(WritableTarget::Pipe(pipe)), - } - } - - pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { - Buffer { - inner: self.inner.buffer(), - has_uncolored_target: self.uncolored_target.is_some(), - } - } - - pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { - if let Some(target) = &self.uncolored_target { - // This impl uses the `eprint` and `print` macros - // instead of `termcolor`'s buffer. - // This is so their output can be captured by `cargo test` - let log = String::from_utf8_lossy(buf.bytes()); - - match target { - WritableTarget::Stderr => eprint!("{}", log), - WritableTarget::Stdout => print!("{}", log), - WritableTarget::Pipe(pipe) => write!(pipe.lock().unwrap(), "{}", log)?, - } - - Ok(()) - } else { - self.inner.print(&buf.inner) - } - } -} - -impl Buffer { - pub(in crate::fmt) fn clear(&mut self) { - self.inner.clear() - } - - pub(in crate::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { - self.inner.write(buf) - } - - pub(in crate::fmt) fn flush(&mut self) -> io::Result<()> { - self.inner.flush() - } - - pub(in crate::fmt) fn bytes(&self) -> &[u8] { - self.inner.as_slice() - } - - fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { - // Ignore styles for test captured logs because they can't be printed - if !self.has_uncolored_target { - self.inner.set_color(spec) - } else { - Ok(()) - } - } - - fn reset(&mut self) -> io::Result<()> { - // Ignore styles for test captured logs because they can't be printed - if !self.has_uncolored_target { - self.inner.reset() - } else { - Ok(()) - } - } -} - -impl WriteStyle { - fn into_color_choice(self) -> ColorChoice { - match self { - WriteStyle::Always => ColorChoice::Always, - WriteStyle::Auto => ColorChoice::Auto, - WriteStyle::Never => ColorChoice::Never, - } - } -} +use super::Buffer; /// A set of styles to apply to the terminal output. /// @@ -240,18 +59,8 @@ impl WriteStyle { /// [`value`]: #method.value #[derive(Clone)] pub struct Style { - buf: Rc>, - spec: ColorSpec, -} - -/// A value that can be printed using the given styles. -/// -/// It is the result of calling [`Style::value`]. -/// -/// [`Style::value`]: struct.Style.html#method.value -pub struct StyledValue<'a, T> { - style: Cow<'a, Style>, - value: T, + pub(in crate::fmt) buf: Rc>, + pub(in crate::fmt) spec: termcolor::ColorSpec, } impl Style { @@ -426,6 +235,22 @@ impl Style { } } +impl fmt::Debug for Style { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Style").field("spec", &self.spec).finish() + } +} + +/// A value that can be printed using the given styles. +/// +/// It is the result of calling [`Style::value`]. +/// +/// [`Style::value`]: struct.Style.html#method.value +pub struct StyledValue<'a, T> { + style: Cow<'a, Style>, + value: T, +} + impl<'a, T> StyledValue<'a, T> { fn write_fmt(&self, f: F) -> fmt::Result where @@ -445,12 +270,6 @@ impl<'a, T> StyledValue<'a, T> { } } -impl fmt::Debug for Style { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Style").field("spec", &self.spec).finish() - } -} - macro_rules! impl_styled_value_fmt { ($($fmt_trait:path),*) => { $( diff --git a/src/fmt/writer/termcolor/mod.rs b/src/fmt/writer/buffer/mod.rs similarity index 51% rename from src/fmt/writer/termcolor/mod.rs rename to src/fmt/writer/buffer/mod.rs index 20f01979..4e678b2d 100644 --- a/src/fmt/writer/termcolor/mod.rs +++ b/src/fmt/writer/buffer/mod.rs @@ -5,8 +5,11 @@ Its public API is available when the `termcolor` crate is available. The terminal printing is shimmed when the `termcolor` crate is not available. */ -#[cfg_attr(feature = "color", path = "extern_impl.rs")] -#[cfg_attr(not(feature = "color"), path = "shim_impl.rs")] -mod imp; - -pub(in crate::fmt) use self::imp::*; +#[cfg(feature = "color")] +mod termcolor; +#[cfg(feature = "color")] +pub(in crate::fmt) use self::termcolor::*; +#[cfg(not(feature = "color"))] +mod plain; +#[cfg(not(feature = "color"))] +pub(in crate::fmt) use plain::*; diff --git a/src/fmt/writer/buffer/plain.rs b/src/fmt/writer/buffer/plain.rs new file mode 100644 index 00000000..e6809b06 --- /dev/null +++ b/src/fmt/writer/buffer/plain.rs @@ -0,0 +1,68 @@ +use std::{io, sync::Mutex}; + +use crate::fmt::{WritableTarget, WriteStyle}; + +pub(in crate::fmt::writer) struct BufferWriter { + target: WritableTarget, +} + +impl BufferWriter { + pub(in crate::fmt::writer) fn stderr(is_test: bool, _write_style: WriteStyle) -> Self { + BufferWriter { + target: if is_test { + WritableTarget::PrintStderr + } else { + WritableTarget::WriteStderr + }, + } + } + + pub(in crate::fmt::writer) fn stdout(is_test: bool, _write_style: WriteStyle) -> Self { + BufferWriter { + target: if is_test { + WritableTarget::PrintStdout + } else { + WritableTarget::WriteStdout + }, + } + } + + pub(in crate::fmt::writer) fn pipe(pipe: Box>) -> Self { + BufferWriter { + target: WritableTarget::Pipe(pipe), + } + } + + pub(in crate::fmt::writer) fn write_style(&self) -> WriteStyle { + WriteStyle::Never + } + + pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { + Buffer(Vec::new()) + } + + pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { + self.target.print(buf) + } +} + +pub(in crate::fmt) struct Buffer(Vec); + +impl Buffer { + pub(in crate::fmt) fn clear(&mut self) { + self.0.clear(); + } + + pub(in crate::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.extend(buf); + Ok(buf.len()) + } + + pub(in crate::fmt) fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + + pub(in crate::fmt) fn as_bytes(&self) -> &[u8] { + &self.0 + } +} diff --git a/src/fmt/writer/buffer/termcolor.rs b/src/fmt/writer/buffer/termcolor.rs new file mode 100644 index 00000000..d3090a17 --- /dev/null +++ b/src/fmt/writer/buffer/termcolor.rs @@ -0,0 +1,108 @@ +use std::io::{self, Write}; +use std::sync::Mutex; + +use termcolor::{self, ColorSpec, WriteColor}; + +use crate::fmt::{WritableTarget, WriteStyle}; + +pub(in crate::fmt::writer) struct BufferWriter { + inner: termcolor::BufferWriter, + uncolored_target: Option, + write_style: WriteStyle, +} + +impl BufferWriter { + pub(in crate::fmt::writer) fn stderr(is_test: bool, write_style: WriteStyle) -> Self { + BufferWriter { + inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()), + uncolored_target: if is_test { + Some(WritableTarget::PrintStderr) + } else { + None + }, + write_style, + } + } + + pub(in crate::fmt::writer) fn stdout(is_test: bool, write_style: WriteStyle) -> Self { + BufferWriter { + inner: termcolor::BufferWriter::stdout(write_style.into_color_choice()), + uncolored_target: if is_test { + Some(WritableTarget::PrintStdout) + } else { + None + }, + write_style, + } + } + + pub(in crate::fmt::writer) fn pipe(pipe: Box>) -> Self { + let write_style = WriteStyle::Never; + BufferWriter { + // The inner Buffer is never printed from, but it is still needed to handle coloring and other formatting + inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()), + uncolored_target: Some(WritableTarget::Pipe(pipe)), + write_style, + } + } + + pub(in crate::fmt::writer) fn write_style(&self) -> WriteStyle { + self.write_style + } + + pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { + Buffer { + inner: self.inner.buffer(), + has_uncolored_target: self.uncolored_target.is_some(), + } + } + + pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { + if let Some(target) = &self.uncolored_target { + target.print(buf) + } else { + self.inner.print(&buf.inner) + } + } +} + +pub(in crate::fmt) struct Buffer { + inner: termcolor::Buffer, + has_uncolored_target: bool, +} + +impl Buffer { + pub(in crate::fmt) fn clear(&mut self) { + self.inner.clear() + } + + pub(in crate::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write(buf) + } + + pub(in crate::fmt) fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } + + pub(in crate::fmt) fn as_bytes(&self) -> &[u8] { + self.inner.as_slice() + } + + pub(in crate::fmt) fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + // Ignore styles for test captured logs because they can't be printed + if !self.has_uncolored_target { + self.inner.set_color(spec) + } else { + Ok(()) + } + } + + pub(in crate::fmt) fn reset(&mut self) -> io::Result<()> { + // Ignore styles for test captured logs because they can't be printed + if !self.has_uncolored_target { + self.inner.reset() + } else { + Ok(()) + } + } +} diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 7f4b6f94..41466b92 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -1,16 +1,15 @@ mod atty; -mod termcolor; +mod buffer; use self::atty::{is_stderr, is_stdout}; -use self::termcolor::BufferWriter; +use self::buffer::BufferWriter; use std::{fmt, io, mem, sync::Mutex}; pub(super) mod glob { - pub use super::termcolor::glob::*; pub use super::*; } -pub(super) use self::termcolor::Buffer; +pub(super) use self::buffer::Buffer; /// Log target, either `stdout`, `stderr` or a custom pipe. #[non_exhaustive] @@ -47,27 +46,49 @@ impl fmt::Debug for Target { /// /// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. pub(super) enum WritableTarget { - /// Logs will be sent to standard output. - Stdout, - /// Logs will be sent to standard error. - Stderr, + /// Logs will be written to standard output. + #[allow(dead_code)] + WriteStdout, + /// Logs will be printed to standard output. + PrintStdout, + /// Logs will be written to standard error. + #[allow(dead_code)] + WriteStderr, + /// Logs will be printed to standard error. + PrintStderr, /// Logs will be sent to a custom pipe. Pipe(Box>), } -impl From for WritableTarget { - fn from(target: Target) -> Self { - match target { - Target::Stdout => Self::Stdout, - Target::Stderr => Self::Stderr, - Target::Pipe(pipe) => Self::Pipe(Box::new(Mutex::new(pipe))), +impl WritableTarget { + fn print(&self, buf: &Buffer) -> io::Result<()> { + use std::io::Write as _; + + let buf = buf.as_bytes(); + match self { + WritableTarget::WriteStdout => { + let stream = std::io::stdout(); + let mut stream = stream.lock(); + stream.write_all(buf)?; + stream.flush()?; + } + WritableTarget::PrintStdout => print!("{}", String::from_utf8_lossy(buf)), + WritableTarget::WriteStderr => { + let stream = std::io::stderr(); + let mut stream = stream.lock(); + stream.write_all(buf)?; + stream.flush()?; + } + WritableTarget::PrintStderr => eprint!("{}", String::from_utf8_lossy(buf)), + // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. + WritableTarget::Pipe(pipe) => { + let mut stream = pipe.lock().unwrap(); + stream.write_all(buf)?; + stream.flush()?; + } } - } -} -impl Default for WritableTarget { - fn default() -> Self { - Self::from(Target::default()) + Ok(()) } } @@ -77,8 +98,10 @@ impl fmt::Debug for WritableTarget { f, "{}", match self { - Self::Stdout => "stdout", - Self::Stderr => "stderr", + Self::WriteStdout => "stdout", + Self::PrintStdout => "stdout", + Self::WriteStderr => "stderr", + Self::PrintStderr => "stderr", Self::Pipe(_) => "pipe", } ) @@ -101,15 +124,25 @@ impl Default for WriteStyle { } } +#[cfg(feature = "color")] +impl WriteStyle { + fn into_color_choice(self) -> ::termcolor::ColorChoice { + match self { + WriteStyle::Always => ::termcolor::ColorChoice::Always, + WriteStyle::Auto => ::termcolor::ColorChoice::Auto, + WriteStyle::Never => ::termcolor::ColorChoice::Never, + } + } +} + /// A terminal target with color awareness. pub(crate) struct Writer { inner: BufferWriter, - write_style: WriteStyle, } impl Writer { pub fn write_style(&self) -> WriteStyle { - self.write_style + self.inner.write_style() } pub(super) fn buffer(&self) -> Buffer { @@ -121,12 +154,18 @@ impl Writer { } } +impl fmt::Debug for Writer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Writer").finish() + } +} + /// A builder for a terminal writer. /// /// The target and style choice can be configured before building. #[derive(Debug)] pub(crate) struct Builder { - target: WritableTarget, + target: Target, write_style: WriteStyle, is_test: bool, built: bool, @@ -145,7 +184,7 @@ impl Builder { /// Set the target to write to. pub(crate) fn target(&mut self, target: Target) -> &mut Self { - self.target = target.into(); + self.target = target; self } @@ -179,9 +218,9 @@ impl Builder { let color_choice = match self.write_style { WriteStyle::Auto => { if match &self.target { - WritableTarget::Stderr => is_stderr(), - WritableTarget::Stdout => is_stdout(), - WritableTarget::Pipe(_) => false, + Target::Stderr => is_stderr(), + Target::Stdout => is_stdout(), + Target::Pipe(_) => false, } { WriteStyle::Auto } else { @@ -190,17 +229,19 @@ impl Builder { } color_choice => color_choice, }; + let color_choice = if self.is_test { + WriteStyle::Never + } else { + color_choice + }; let writer = match mem::take(&mut self.target) { - WritableTarget::Stderr => BufferWriter::stderr(self.is_test, color_choice), - WritableTarget::Stdout => BufferWriter::stdout(self.is_test, color_choice), - WritableTarget::Pipe(pipe) => BufferWriter::pipe(color_choice, pipe), + Target::Stderr => BufferWriter::stderr(self.is_test, color_choice), + Target::Stdout => BufferWriter::stdout(self.is_test, color_choice), + Target::Pipe(pipe) => BufferWriter::pipe(Box::new(Mutex::new(pipe))), }; - Writer { - inner: writer, - write_style: self.write_style, - } + Writer { inner: writer } } } @@ -210,12 +251,6 @@ impl Default for Builder { } } -impl fmt::Debug for Writer { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Writer").finish() - } -} - fn parse_write_style(spec: &str) -> WriteStyle { match spec { "auto" => WriteStyle::Auto, diff --git a/src/fmt/writer/termcolor/shim_impl.rs b/src/fmt/writer/termcolor/shim_impl.rs deleted file mode 100644 index 0705770c..00000000 --- a/src/fmt/writer/termcolor/shim_impl.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::{io, sync::Mutex}; - -use crate::fmt::{WritableTarget, WriteStyle}; - -pub(in crate::fmt::writer) mod glob {} - -pub(in crate::fmt::writer) struct BufferWriter { - target: WritableTarget, -} - -pub(in crate::fmt) struct Buffer(Vec); - -impl BufferWriter { - pub(in crate::fmt::writer) fn stderr(_is_test: bool, _write_style: WriteStyle) -> Self { - BufferWriter { - target: WritableTarget::Stderr, - } - } - - pub(in crate::fmt::writer) fn stdout(_is_test: bool, _write_style: WriteStyle) -> Self { - BufferWriter { - target: WritableTarget::Stdout, - } - } - - pub(in crate::fmt::writer) fn pipe( - _write_style: WriteStyle, - pipe: Box>, - ) -> Self { - BufferWriter { - target: WritableTarget::Pipe(pipe), - } - } - - pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { - Buffer(Vec::new()) - } - - pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { - // This impl uses the `eprint` and `print` macros - // instead of using the streams directly. - // This is so their output can be captured by `cargo test`. - match &self.target { - // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. - WritableTarget::Pipe(pipe) => pipe.lock().unwrap().write_all(&buf.0)?, - WritableTarget::Stdout => print!("{}", String::from_utf8_lossy(&buf.0)), - WritableTarget::Stderr => eprint!("{}", String::from_utf8_lossy(&buf.0)), - } - - Ok(()) - } -} - -impl Buffer { - pub(in crate::fmt) fn clear(&mut self) { - self.0.clear(); - } - - pub(in crate::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend(buf); - Ok(buf.len()) - } - - pub(in crate::fmt) fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - - #[cfg(test)] - pub(in crate::fmt) fn bytes(&self) -> &[u8] { - &self.0 - } -}