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 }