github.com/grahambrereton-form3/tilt@v0.10.18/internal/rty/ansi.go (about) 1 package rty 2 3 import ( 4 "bytes" 5 "fmt" 6 "strconv" 7 "strings" 8 9 "github.com/gdamore/tcell" 10 ) 11 12 // this is adapted from tview. 13 14 // The states of the ANSI escape code parser. 15 const ( 16 ansiText = iota 17 ansiEscape 18 ansiSubstring 19 ansiControlSequence 20 ) 21 22 // ansi is a io.Writer which translates ANSI escape codes into tview color 23 // tags. 24 type ansi struct { 25 buffer []rune 26 directives []directive 27 28 // Reusable buffers. 29 csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings. 30 31 // The current state of the parser. One of the ansi constants. 32 state int 33 } 34 35 // ANSIWriter returns an io.Writer which translates any ANSI escape codes 36 // written to it into tview color tags. Other escape codes don't have an effect 37 // and are simply removed. The translated text is written to the provided 38 // writer. 39 func ANSIWriter() *ansi { 40 return &ansi{ 41 csiParameter: new(bytes.Buffer), 42 csiIntermediate: new(bytes.Buffer), 43 state: ansiText, 44 } 45 } 46 47 func (a *ansi) setColors(fg, bg string) { 48 a.Flush() 49 a.directives = append(a.directives, fgDirective(tcell.GetColor(fg))) 50 a.directives = append(a.directives, bgDirective(tcell.GetColor(bg))) 51 } 52 53 func (a *ansi) Flush() { 54 if len(a.buffer) == 0 { 55 return 56 } 57 a.directives = append(a.directives, textDirective(string(a.buffer))) 58 a.buffer = nil 59 } 60 61 // Write parses the given text as a string of runes, translates ANSI escape 62 // codes to color tags and writes them to the output writer. 63 func (a *ansi) Write(text []byte) (int, error) { 64 defer func() { 65 a.Flush() 66 }() 67 68 for _, r := range string(text) { 69 switch a.state { 70 71 // We just entered an escape sequence. 72 case ansiEscape: 73 switch r { 74 case '[': // Control Sequence Introducer. 75 a.csiParameter.Reset() 76 a.csiIntermediate.Reset() 77 a.state = ansiControlSequence 78 case 'c': // Reset. 79 a.setColors("-", "-") 80 a.state = ansiText 81 case 'P', ']', 'X', '^', '_': // Substrings and commands. 82 a.state = ansiSubstring 83 default: // Ignore. 84 a.state = ansiText 85 } 86 87 // CSI Sequences. 88 case ansiControlSequence: 89 switch { 90 case r >= 0x30 && r <= 0x3f: // Parameter bytes. 91 if _, err := a.csiParameter.WriteRune(r); err != nil { 92 return 0, err 93 } 94 case r >= 0x20 && r <= 0x2f: // Intermediate bytes. 95 if _, err := a.csiIntermediate.WriteRune(r); err != nil { 96 return 0, err 97 } 98 case r >= 0x40 && r <= 0x7e: // Final byte. 99 switch r { 100 case 'E': // Next line. 101 count, _ := strconv.Atoi(a.csiParameter.String()) 102 if count == 0 { 103 count = 1 104 } 105 a.buffer = append(a.buffer, []rune(strings.Repeat("\n", count))...) 106 case 'm': // Select Graphic Rendition. 107 var ( 108 background, foreground, attributes string 109 clearAttributes bool 110 ) 111 fields := strings.Split(a.csiParameter.String(), ";") 112 if len(fields) == 0 || len(fields) == 1 && fields[0] == "0" { 113 // Reset. 114 a.setColors("-", "-") 115 break 116 } 117 lookupColor := func(colorNumber int, bright bool) string { 118 if colorNumber < 0 || colorNumber > 7 { 119 return "black" 120 } 121 if bright { 122 colorNumber += 8 123 } 124 return [...]string{ 125 "black", 126 "red", 127 "green", 128 "yellow", 129 "blue", 130 "darkmagenta", 131 "darkcyan", 132 "white", 133 "#7f7f7f", 134 "#ff0000", 135 "#00ff00", 136 "#ffff00", 137 "#5c5cff", 138 "#ff00ff", 139 "#00ffff", 140 "#ffffff", 141 }[colorNumber] 142 } 143 for index, field := range fields { 144 switch field { 145 case "1", "01": 146 attributes += "b" 147 case "2", "02": 148 attributes += "d" 149 case "4", "04": 150 attributes += "u" 151 case "5", "05": 152 attributes += "l" 153 case "7", "07": 154 attributes += "7" 155 case "22", "24", "25", "27": 156 clearAttributes = true 157 case "30", "31", "32", "33", "34", "35", "36", "37": 158 colorNumber, _ := strconv.Atoi(field) 159 foreground = lookupColor(colorNumber-30, false) 160 case "40", "41", "42", "43", "44", "45", "46", "47": 161 colorNumber, _ := strconv.Atoi(field) 162 background = lookupColor(colorNumber-40, false) 163 case "90", "91", "92", "93", "94", "95", "96", "97": 164 colorNumber, _ := strconv.Atoi(field) 165 foreground = lookupColor(colorNumber-90, true) 166 case "100", "101", "102", "103", "104", "105", "106", "107": 167 colorNumber, _ := strconv.Atoi(field) 168 background = lookupColor(colorNumber-100, true) 169 case "38", "48": 170 var color string 171 if len(fields) > index+1 { 172 if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors. 173 colorNumber, _ := strconv.Atoi(fields[index+2]) 174 if colorNumber <= 7 { 175 color = lookupColor(colorNumber, false) 176 } else if colorNumber <= 15 { 177 color = lookupColor(colorNumber, true) 178 } else if colorNumber <= 231 { 179 red := (colorNumber - 16) / 36 180 green := ((colorNumber - 16) / 6) % 6 181 blue := (colorNumber - 16) % 6 182 color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5) 183 } else if colorNumber <= 255 { 184 grey := 255 * (colorNumber - 232) / 23 185 color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey) 186 } 187 } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors. 188 red, _ := strconv.Atoi(fields[index+2]) 189 green, _ := strconv.Atoi(fields[index+3]) 190 blue, _ := strconv.Atoi(fields[index+4]) 191 color = fmt.Sprintf("#%02x%02x%02x", red, green, blue) 192 } 193 } 194 if len(color) > 0 { 195 if field == "38" { 196 foreground = color 197 } else { 198 background = color 199 } 200 } 201 } 202 } 203 if len(attributes) > 0 || clearAttributes { 204 attributes = ":" + attributes 205 } 206 if len(foreground) > 0 || len(background) > 0 || len(attributes) > 0 { 207 a.setColors(foreground, background) 208 } 209 } 210 a.state = ansiText 211 default: // Undefined byte. 212 a.state = ansiText // Abort CSI. 213 } 214 215 // We just entered a substring/command sequence. 216 case ansiSubstring: 217 if r == 27 { // Most likely the end of the substring. 218 a.state = ansiEscape 219 } // Ignore all other characters. 220 221 // "ansiText" and all others. 222 default: 223 if r == 27 { 224 // This is the start of an escape sequence. 225 a.state = ansiEscape 226 } else { 227 // Just a regular rune. Send to buffer. 228 a.buffer = append(a.buffer, r) 229 } 230 } 231 } 232 233 return len(text), nil 234 }