ansi.go (8371B)
1 package tview 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "strconv" 8 "strings" 9 ) 10 11 // The states of the ANSI escape code parser. 12 const ( 13 ansiText = iota 14 ansiEscape 15 ansiSubstring 16 ansiControlSequence 17 ) 18 19 // ansi is a io.Writer which translates ANSI escape codes into tview color 20 // tags. 21 type ansi struct { 22 io.Writer 23 24 // Reusable buffers. 25 buffer *bytes.Buffer // The entire output text of one Write(). 26 csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings. 27 attributes string // The buffer's current text attributes (a tview attribute string). 28 29 // The current state of the parser. One of the ansi constants. 30 state int 31 } 32 33 // ANSIWriter returns an io.Writer which translates any ANSI escape codes 34 // written to it into tview style tags. Other escape codes don't have an effect 35 // and are simply removed. The translated text is written to the provided 36 // writer. 37 func ANSIWriter(writer io.Writer) io.Writer { 38 return &ansi{ 39 Writer: writer, 40 buffer: new(bytes.Buffer), 41 csiParameter: new(bytes.Buffer), 42 csiIntermediate: new(bytes.Buffer), 43 state: ansiText, 44 } 45 } 46 47 // Write parses the given text as a string of runes, translates ANSI escape 48 // codes to style tags and writes them to the output writer. 49 func (a *ansi) Write(text []byte) (int, error) { 50 defer func() { 51 a.buffer.Reset() 52 }() 53 54 for _, r := range string(text) { 55 switch a.state { 56 57 // We just entered an escape sequence. 58 case ansiEscape: 59 switch r { 60 case '[': // Control Sequence Introducer. 61 a.csiParameter.Reset() 62 a.csiIntermediate.Reset() 63 a.state = ansiControlSequence 64 case 'c': // Reset. 65 fmt.Fprint(a.buffer, "[-:-:-]") 66 a.state = ansiText 67 case 'P', ']', 'X', '^', '_': // Substrings and commands. 68 a.state = ansiSubstring 69 default: // Ignore. 70 a.state = ansiText 71 } 72 73 // CSI Sequences. 74 case ansiControlSequence: 75 switch { 76 case r >= 0x30 && r <= 0x3f: // Parameter bytes. 77 if _, err := a.csiParameter.WriteRune(r); err != nil { 78 return 0, err 79 } 80 case r >= 0x20 && r <= 0x2f: // Intermediate bytes. 81 if _, err := a.csiIntermediate.WriteRune(r); err != nil { 82 return 0, err 83 } 84 case r >= 0x40 && r <= 0x7e: // Final byte. 85 switch r { 86 case 'E': // Next line. 87 count, _ := strconv.Atoi(a.csiParameter.String()) 88 if count == 0 { 89 count = 1 90 } 91 fmt.Fprint(a.buffer, strings.Repeat("\n", count)) 92 case 'm': // Select Graphic Rendition. 93 var background, foreground string 94 params := a.csiParameter.String() 95 fields := strings.Split(params, ";") 96 if len(params) == 0 || fields[0] == "" || fields[0] == "0" { 97 // Reset. 98 foreground = "-" 99 background = "-" 100 a.attributes = "-" 101 } 102 lookupColor := func(colorNumber int) string { 103 if colorNumber < 0 || colorNumber > 15 { 104 return "black" 105 } 106 return []string{ 107 "black", 108 "maroon", 109 "green", 110 "olive", 111 "navy", 112 "purple", 113 "teal", 114 "silver", 115 "gray", 116 "red", 117 "lime", 118 "yellow", 119 "blue", 120 "fuchsia", 121 "aqua", 122 "white", 123 }[colorNumber] 124 } 125 FieldLoop: 126 for index, field := range fields { 127 switch field { 128 case "1", "01": 129 if !strings.ContainsRune(a.attributes, 'b') { 130 a.attributes += "b" 131 } 132 case "2", "02": 133 if !strings.ContainsRune(a.attributes, 'd') { 134 a.attributes += "d" 135 } 136 case "3", "03": 137 if !strings.ContainsRune(a.attributes, 'i') { 138 a.attributes += "i" 139 } 140 case "4", "04": 141 if !strings.ContainsRune(a.attributes, 'u') { 142 a.attributes += "u" 143 } 144 case "5", "05": 145 if !strings.ContainsRune(a.attributes, 'l') { 146 a.attributes += "l" 147 } 148 case "7", "07": 149 if !strings.ContainsRune(a.attributes, 'r') { 150 a.attributes += "r" 151 } 152 case "9", "09": 153 if !strings.ContainsRune(a.attributes, 's') { 154 a.attributes += "s" 155 } 156 case "22": 157 if i := strings.IndexRune(a.attributes, 'b'); i >= 0 { 158 a.attributes = a.attributes[:i] + a.attributes[i+1:] 159 } 160 if i := strings.IndexRune(a.attributes, 'd'); i >= 0 { 161 a.attributes = a.attributes[:i] + a.attributes[i+1:] 162 } 163 case "23": 164 if i := strings.IndexRune(a.attributes, 'i'); i >= 0 { 165 a.attributes = a.attributes[:i] + a.attributes[i+1:] 166 } 167 case "24": 168 if i := strings.IndexRune(a.attributes, 'u'); i >= 0 { 169 a.attributes = a.attributes[:i] + a.attributes[i+1:] 170 } 171 case "25": 172 if i := strings.IndexRune(a.attributes, 'l'); i >= 0 { 173 a.attributes = a.attributes[:i] + a.attributes[i+1:] 174 } 175 case "27": 176 if i := strings.IndexRune(a.attributes, 'r'); i >= 0 { 177 a.attributes = a.attributes[:i] + a.attributes[i+1:] 178 } 179 case "29": 180 if i := strings.IndexRune(a.attributes, 's'); i >= 0 { 181 a.attributes = a.attributes[:i] + a.attributes[i+1:] 182 } 183 case "30", "31", "32", "33", "34", "35", "36", "37": 184 colorNumber, _ := strconv.Atoi(field) 185 foreground = lookupColor(colorNumber - 30) 186 case "39": 187 foreground = "-" 188 case "40", "41", "42", "43", "44", "45", "46", "47": 189 colorNumber, _ := strconv.Atoi(field) 190 background = lookupColor(colorNumber - 40) 191 case "49": 192 background = "-" 193 case "90", "91", "92", "93", "94", "95", "96", "97": 194 colorNumber, _ := strconv.Atoi(field) 195 foreground = lookupColor(colorNumber - 82) 196 case "100", "101", "102", "103", "104", "105", "106", "107": 197 colorNumber, _ := strconv.Atoi(field) 198 background = lookupColor(colorNumber - 92) 199 case "38", "48": 200 var color string 201 if len(fields) > index+1 { 202 if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors. 203 colorNumber, _ := strconv.Atoi(fields[index+2]) 204 if colorNumber <= 15 { 205 color = lookupColor(colorNumber) 206 } else if colorNumber <= 231 { 207 red := (colorNumber - 16) / 36 208 green := ((colorNumber - 16) / 6) % 6 209 blue := (colorNumber - 16) % 6 210 color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5) 211 } else if colorNumber <= 255 { 212 grey := 255 * (colorNumber - 232) / 23 213 color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey) 214 } 215 } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors. 216 red, _ := strconv.Atoi(fields[index+2]) 217 green, _ := strconv.Atoi(fields[index+3]) 218 blue, _ := strconv.Atoi(fields[index+4]) 219 color = fmt.Sprintf("#%02x%02x%02x", red, green, blue) 220 } 221 } 222 if len(color) > 0 { 223 if field == "38" { 224 foreground = color 225 } else { 226 background = color 227 } 228 } 229 break FieldLoop 230 } 231 } 232 var colon string 233 if len(a.attributes) > 1 && a.attributes[0] == '-' { 234 a.attributes = a.attributes[1:] 235 } 236 if len(a.attributes) > 0 { 237 colon = ":" 238 } 239 if len(foreground) > 0 || len(background) > 0 || len(a.attributes) > 0 { 240 fmt.Fprintf(a.buffer, "[%s:%s%s%s]", foreground, background, colon, a.attributes) 241 } 242 } 243 a.state = ansiText 244 default: // Undefined byte. 245 a.state = ansiText // Abort CSI. 246 } 247 248 // We just entered a substring/command sequence. 249 case ansiSubstring: 250 if r == 27 { // Most likely the end of the substring. 251 a.state = ansiEscape 252 } // Ignore all other characters. 253 254 // "ansiText" and all others. 255 default: 256 if r == 27 { 257 // This is the start of an escape sequence. 258 a.state = ansiEscape 259 } else { 260 // Just a regular rune. Send to buffer. 261 if _, err := a.buffer.WriteRune(r); err != nil { 262 return 0, err 263 } 264 } 265 } 266 } 267 268 // Write buffer to target writer. 269 n, err := a.buffer.WriteTo(a.Writer) 270 if err != nil { 271 return int(n), err 272 } 273 return len(text), nil 274 } 275 276 // TranslateANSI replaces ANSI escape sequences found in the provided string 277 // with tview's style tags and returns the resulting string. 278 func TranslateANSI(text string) string { 279 var buffer bytes.Buffer 280 writer := ANSIWriter(&buffer) 281 writer.Write([]byte(text)) 282 return buffer.String() 283 }