use std::{
env, fmt,
io::{self, IsTerminal, Write},
ops,
};
use crate::text::Wrapper;
fn enable_color<T: IsTerminal>(fd: &T) -> bool {
if env::var_os("NO_COLOR").is_some() {
return false;
}
if env::var_os("CLICOLOR_FORCE").is_some() {
return true;
}
if let Some(term) = env::var_os("TERM")
&& term == "dumb"
{
return false;
}
fd.is_terminal()
}
struct FmtAdapter<F: fmt::Write>(F);
impl<F: fmt::Write> io::Write for FmtAdapter<F> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for chunk in buf.utf8_chunks() {
self.0
.write_str(chunk.valid())
.map_err(|_| io::ErrorKind::Other)?;
if !chunk.invalid().is_empty() {
self.0
.write_char('\u{fffe}')
.map_err(|_| io::ErrorKind::Other)?;
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
pub const DEFAULT_LINE_WIDTH: usize = 100;
pub struct Writer<'a> {
out: Box<dyn Write + 'a>,
enable_color: bool,
max_line_width: usize,
paragraph: Vec<u8>,
indentation: Vec<u8>,
}
impl<'a> Writer<'a> {
pub fn display(f: impl fmt::Write + 'a) -> Self {
Self::new(FmtAdapter(f), false)
}
pub fn fd(fd: impl Write + IsTerminal + 'a) -> Self {
let colors = enable_color(&fd);
Self::new(fd, colors)
}
pub fn io(w: impl Write + 'a) -> Self {
Self::new(w, false)
}
fn new(out: impl Write + 'a, enable_color: bool) -> Self {
Self {
out: Box::new(out),
enable_color,
max_line_width: DEFAULT_LINE_WIDTH,
paragraph: Vec::new(),
indentation: vec![b'\n'],
}
}
pub fn max_line_width(&self) -> usize {
self.max_line_width
}
pub fn set_max_line_width(&mut self, width: usize) {
self.max_line_width = width;
}
pub fn force_color(&mut self, color: bool) {
self.enable_color = color;
}
pub fn indentation(&self) -> usize {
self.indentation.len() - 1
}
pub fn set_indentation(&mut self, indent: usize) {
self.indentation.resize(indent + 1, b' ');
}
fn finish_paragraph(&mut self) -> io::Result<()> {
let iter = Wrapper::new(&self.paragraph, self.max_line_width)
.with_newline(&self.indentation, self.indentation.len() - 1)
.wrap();
for frag in iter {
for chunk in frag.utf8_chunks() {
for (i, ch) in chunk.valid().char_indices() {
match ch {
NBSP => self.out.write_all(b" ")?,
ZWSP => continue,
_ => self
.out
.write_all(&chunk.valid().as_bytes()[i..i + ch.len_utf8()])?,
}
}
if !chunk.invalid().is_empty() {
self.out.write_all(frag)?;
}
}
}
self.paragraph.clear();
Ok(())
}
}
impl Write for Writer<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for chunk in buf.utf8_chunks() {
for line in chunk.valid().split_inclusive('\n') {
if self.enable_color {
self.paragraph.extend_from_slice(line.as_bytes());
} else {
strip_escapes(line.as_bytes(), &mut self.paragraph);
}
if line.ends_with('\n') {
self.finish_paragraph()?;
}
}
if !chunk.invalid().is_empty() {
self.paragraph.extend_from_slice("\u{fffe}".as_bytes());
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.finish_paragraph()?;
self.out.flush()
}
}
fn strip_escapes(input: &[u8], output: &mut Vec<u8>) {
let mut in_escape = false;
for &byte in input {
match byte {
b'\x1b' => in_escape = true,
b'm' if in_escape => in_escape = false,
_ if !in_escape => output.push(byte),
_ => {}
}
}
}
impl Drop for Writer<'_> {
fn drop(&mut self) {
self.finish_paragraph().ok();
}
}
pub const NBSP: char = '\u{00A0}';
pub const ZWSP: char = '\u{200B}';
const fn style(bit: u64) -> Style {
Style(1 << bit)
}
pub const RESET: Style = style(0);
pub const BOLD: Style = style(1);
pub const FAINT: Style = style(2);
pub const ITALIC: Style = style(3);
pub const UNDERLINE: Style = style(4);
pub const RED: Style = style(31);
pub const YELLOW: Style = style(33);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Style(pub u64);
impl Style {
pub const NONE: Style = Style(0);
}
impl ops::BitOr for Style {
type Output = Style;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl ops::BitOrAssign for Style {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl fmt::Display for Style {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0 == 0 {
return Ok(());
}
let mut buf = [0; 128];
let mut w = &mut buf[..];
write!(w, "\x1B[").ok();
let mut first = true;
for bit in 0..64 {
if self.0 & (1 << bit) != 0 {
if !first {
write!(w, ";").ok();
}
first = false;
write!(w, "{bit}").ok();
}
}
write!(w, "m").ok();
let remaining = w.len();
let used = buf.len() - remaining;
f.write_str(str::from_utf8(&buf[..used]).unwrap())
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex, PoisonError};
use super::*;
#[derive(Default, Clone)]
struct TestSink(Arc<Mutex<Vec<u8>>>);
impl Write for TestSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.flush()
}
}
#[track_caller]
fn col_check(color: bool, f: impl FnOnce(&mut Writer), raw: &[u8]) {
let sink = TestSink::default();
let mut w = Writer::new(sink.clone(), color);
f(&mut w);
w.flush().unwrap();
let guard = sink.0.lock().unwrap();
assert_eq!(
raw,
guard.as_slice(),
r#"expected "{}", got "{}""#,
raw.escape_ascii(),
guard.as_slice().escape_ascii()
);
}
#[track_caller]
fn wrapcheck(f: impl FnOnce(&mut Writer), raw: &[u8]) {
col_check(
true,
|w| {
w.max_line_width = 5;
f(w)
},
raw,
);
}
#[test]
fn colors() {
col_check(false, |w| writeln!(w, "abcde").unwrap(), b"abcde\n");
col_check(true, |w| writeln!(w, "abcde").unwrap(), b"abcde\n");
col_check(false, |w| writeln!(w, "A{BOLD}B").unwrap(), b"AB\n");
col_check(true, |w| writeln!(w, "A{BOLD}B").unwrap(), b"A\x1B[1mB\n");
}
#[test]
fn test_wrapping() {
wrapcheck(
|w| write!(w, "abc def 1 2 34 56").unwrap(),
b"abc\ndef 1\n2 34\n56",
);
}
#[test]
fn test_indentation() {
wrapcheck(
|w| {
w.max_line_width = 10;
w.set_indentation(5);
write!(w, "123 456 789 abc").unwrap();
},
b"123 456\n 789\n abc",
);
}
#[test]
fn test_style() {
assert_eq!(Style::NONE.to_string(), "");
assert_eq!((RED | UNDERLINE).to_string(), "\x1B[4;31m");
assert_eq!(RED.to_string(), "\x1B[31m");
}
}