github.com/BarDweller/libpak@v0.0.0-20230630201634-8dd5cfc15ec9/bard/writer.go (about)

     1  /*
     2   * Copyright 2018-2020 the original author or authors.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *      https://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package bard
    18  
    19  import (
    20  	"bytes"
    21  	"io"
    22  	"strings"
    23  
    24  	"github.com/heroku/color"
    25  )
    26  
    27  const (
    28  	escape     = "\x1b["
    29  	endCode    = "m"
    30  	delimiter  = ";"
    31  	colorReset = "\x1b[0m"
    32  )
    33  
    34  // Writer is an object that will indent and color all output flowing through it.
    35  type Writer struct {
    36  	code         string
    37  	color        *color.Color
    38  	indent       int
    39  	shouldIndent bool
    40  	writer       io.Writer
    41  }
    42  
    43  // NewWriter creates a instance that wraps another writer.
    44  func NewWriter(writer io.Writer, options ...WriterOption) *Writer {
    45  	w := Writer{writer: writer, shouldIndent: true}
    46  	for _, option := range options {
    47  		w = option(w)
    48  	}
    49  
    50  	return &w
    51  }
    52  
    53  func (w *Writer) Write(b []byte) (int, error) {
    54  	var (
    55  		prefix, suffix []byte
    56  		reset          = []byte("\r")
    57  		newline        = []byte("\n")
    58  		n              = len(b)
    59  	)
    60  
    61  	if bytes.HasPrefix(b, reset) {
    62  		b = bytes.TrimPrefix(b, reset)
    63  		prefix = reset
    64  	}
    65  
    66  	if bytes.HasSuffix(b, newline) {
    67  		b = bytes.TrimSuffix(b, newline)
    68  		suffix = newline
    69  	}
    70  
    71  	lines := bytes.Split(b, newline)
    72  
    73  	var indentedLines [][]byte
    74  	for i, line := range lines {
    75  		if w.shouldIndent || i > 0 {
    76  			for i := 0; i < w.indent; i++ {
    77  				line = append([]byte("  "), line...)
    78  			}
    79  			w.shouldIndent = false
    80  		}
    81  
    82  		if w.color != nil {
    83  			s := string(line)
    84  			s = strings.ReplaceAll(s, colorReset, colorReset+w.code)
    85  			line = []byte(w.color.Sprint(s))
    86  		}
    87  
    88  		indentedLines = append(indentedLines, line)
    89  	}
    90  
    91  	b = bytes.Join(indentedLines, newline)
    92  
    93  	if prefix != nil {
    94  		b = append(prefix, b...)
    95  	}
    96  
    97  	if suffix != nil {
    98  		b = append(b, suffix...)
    99  	}
   100  
   101  	if bytes.HasSuffix(b, newline) {
   102  		w.shouldIndent = true
   103  	}
   104  
   105  	if _, err := w.writer.Write(b); err != nil {
   106  		return n, err
   107  	}
   108  
   109  	return n, nil
   110  }
   111  
   112  // WriterOption is a function for configuring a Writer instance.
   113  type WriterOption func(Writer) Writer
   114  
   115  // WithAttributes creates an WriterOption that sets the output color.
   116  func WithAttributes(attributes ...color.Attribute) WriterOption {
   117  	return func(l Writer) Writer {
   118  		l.code = chainSGRCodes(attributes)
   119  		l.color = color.New(attributes...)
   120  		return l
   121  	}
   122  }
   123  
   124  // WithIndent creates an WriterOption that sets the depth of the output indent.
   125  func WithIndent(indent int) WriterOption {
   126  	return func(l Writer) Writer {
   127  		l.indent = indent
   128  		return l
   129  	}
   130  }
   131  
   132  func chainSGRCodes(a []color.Attribute) string {
   133  	codes := toCodes(a)
   134  
   135  	if len(codes) == 0 {
   136  		return colorReset
   137  	}
   138  
   139  	if len(codes) == 1 {
   140  		return escape + codes[0] + endCode
   141  	}
   142  
   143  	var b strings.Builder
   144  	b.Grow((len(codes) * 2) + len(escape) + len(endCode))
   145  	b.WriteString(escape)
   146  
   147  	delimsAdded := 0
   148  	for i := 0; i < len(a); i++ {
   149  		if delimsAdded > 0 {
   150  			_, _ = b.WriteString(delimiter)
   151  		}
   152  		b.WriteString(codes[i])
   153  		delimsAdded++
   154  	}
   155  
   156  	b.WriteString(endCode)
   157  
   158  	return b.String()
   159  }
   160  
   161  func toCodes(attrs []color.Attribute) []string {
   162  	var codes []string
   163  
   164  	for _, a := range attrs {
   165  		codes = append(codes, a.String())
   166  	}
   167  
   168  	return codes
   169  }