use std::io::{self, IsTerminal, Write};
use super::LogSink;
use crate::level::Level;
const RESET: &[u8] = b"\x1b[0m";
fn color_code(level: Level) -> &'static [u8] {
match level {
Level::Error => b"\x1b[91m", Level::Warn => b"\x1b[33m", Level::Info => b"\x1b[32m", Level::Debug => b"\x1b[36m", Level::Trace => b"\x1b[90m", }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorMode {
#[default]
Auto,
Always,
Never,
}
fn resolve(mode: ColorMode, stream: &Stream) -> bool {
match mode {
ColorMode::Always => true,
ColorMode::Never => false,
ColorMode::Auto => stream.is_terminal(),
}
}
fn render(line: &[u8], level: Level, color: bool, out: &mut Vec<u8>) {
out.clear();
if color {
out.extend_from_slice(color_code(level));
out.extend_from_slice(line);
out.extend_from_slice(RESET);
} else {
out.extend_from_slice(line);
}
out.push(b'\n');
}
enum Stream {
Stdout(io::Stdout),
Stderr(io::Stderr),
}
impl Stream {
fn is_terminal(&self) -> bool {
match self {
Stream::Stdout(s) => s.is_terminal(),
Stream::Stderr(s) => s.is_terminal(),
}
}
fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {
match self {
Stream::Stdout(s) => s.lock().write_all(bytes),
Stream::Stderr(s) => s.lock().write_all(bytes),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Stream::Stdout(s) => s.lock().flush(),
Stream::Stderr(s) => s.lock().flush(),
}
}
}
pub struct ConsoleSink {
stream: Stream,
mode: ColorMode,
use_color: bool,
buf: Vec<u8>,
}
impl ConsoleSink {
fn with_stream(stream: Stream) -> Self {
let use_color = resolve(ColorMode::Auto, &stream);
Self {
stream,
mode: ColorMode::Auto,
use_color,
buf: Vec::new(),
}
}
pub fn stdout() -> Self {
Self::with_stream(Stream::Stdout(io::stdout()))
}
pub fn stderr() -> Self {
Self::with_stream(Stream::Stderr(io::stderr()))
}
pub fn with_color(mut self, mode: ColorMode) -> Self {
self.mode = mode;
self.use_color = resolve(mode, &self.stream);
self
}
}
impl LogSink for ConsoleSink {
fn accept(&mut self, line: &[u8], level: Level) -> io::Result<()> {
render(line, level, self.use_color, &mut self.buf);
self.stream.write_all(&self.buf)
}
fn flush(&mut self) -> io::Result<()> {
self.stream.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_plain_is_line_plus_newline() {
let mut out = Vec::new();
render(b"hello", Level::Info, false, &mut out);
assert_eq!(out, b"hello\n");
}
#[test]
fn render_colored_wraps_line_in_code_and_reset() {
let mut out = Vec::new();
render(b"boom", Level::Error, true, &mut out);
let mut expected = Vec::new();
expected.extend_from_slice(b"\x1b[91m");
expected.extend_from_slice(b"boom");
expected.extend_from_slice(RESET);
expected.push(b'\n');
assert_eq!(out, expected);
}
#[test]
fn render_clears_prior_contents() {
let mut out = b"stale".to_vec();
render(b"fresh", Level::Info, false, &mut out);
assert_eq!(out, b"fresh\n");
}
#[test]
fn color_code_is_distinct_per_level() {
let codes = [
color_code(Level::Error),
color_code(Level::Warn),
color_code(Level::Info),
color_code(Level::Debug),
color_code(Level::Trace),
];
for (i, a) in codes.iter().enumerate() {
for b in &codes[i + 1..] {
assert_ne!(a, b, "two levels share a color code");
}
}
}
#[test]
fn resolve_always_and_never_ignore_terminal() {
let stream = Stream::Stdout(io::stdout());
assert!(resolve(ColorMode::Always, &stream));
assert!(!resolve(ColorMode::Never, &stream));
}
#[test]
fn resolve_auto_follows_terminal_status() {
let stream = Stream::Stdout(io::stdout());
assert_eq!(resolve(ColorMode::Auto, &stream), stream.is_terminal());
}
#[test]
fn default_color_mode_is_auto() {
assert_eq!(ColorMode::default(), ColorMode::Auto);
}
#[test]
fn with_color_overrides_resolution() {
let sink = ConsoleSink::stdout().with_color(ColorMode::Always);
assert_eq!(sink.mode, ColorMode::Always);
assert!(sink.use_color);
let sink = ConsoleSink::stdout().with_color(ColorMode::Never);
assert!(!sink.use_color);
}
#[test]
fn new_sink_defaults_to_auto_mode() {
let sink = ConsoleSink::stderr();
assert_eq!(sink.mode, ColorMode::Auto);
}
}