github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekalog/encoder_console_color_builder.go (about) 1 // Copyright © 2020. All rights reserved. 2 // Author: Ilya Stroy. 3 // Contacts: iyuryevich@pm.me, https://github.com/qioalice 4 // License: https://opensource.org/licenses/MIT 5 6 package ekalog 7 8 import ( 9 "image/color" 10 "strconv" 11 "strings" 12 13 "github.com/qioalice/ekago/v3/internal/3rdparty/xtermcolor" 14 ) 15 16 type ( 17 // colorBuilder is a helper to construct bash color escape sequence 18 // (like "\033[01;03;38;05;144m") to represent a some color. 19 // 20 // Objects of this type are instantiated at the color verbs parsing, their 21 // fields are filed by values provided at the verb and then bash sequence 22 // is generated by encode(). 23 // 24 // See https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters for more info. 25 colorBuilder struct { 26 27 // if bg == 100 it means full reset to default colors is requested 28 // no one next verb's part will be processed 29 30 // [0..255] - xterm256 ANSI SGR color code 31 // -1 if 'do cleanup color to terminal default' ( "\033[39m" or "\033[49m" ) 32 // -2 if 'not set, use those one that was used' (not included to SGR) 33 bg, fg int16 34 35 // 0 - 'not set, use those one that was used' (not included to SGR) 36 // 1 - enable (included to SGR (01/03/04)) 37 // -1 - disable (included to SGR (22/23/24)) 38 bold, italic, underline int8 39 40 // Yes, there is no support blinking text. 41 // I think it's disgusting. It will never be added. 42 } 43 ) 44 45 func (cb *colorBuilder) init() { 46 cb.bg, cb.fg = -2, -2 47 cb.bold, cb.italic, cb.underline = 0, 0, 0 48 } 49 50 func (cb *colorBuilder) parseEntity(verbPart string) (parsed bool) { 51 52 if cb.bg == 100 { 53 return true 54 } 55 56 switch verbPart = strings.ToUpper(strings.TrimSpace(verbPart)); verbPart { 57 // --- REMINDER! 1ST ARGUMENT IS ALWAYS UPPER CASED! --- 58 59 case "": 60 return true 61 62 case "0": 63 cb.bg = 100 64 return true 65 66 case "BOLD", "B": 67 cb.bold = 1 68 return true 69 70 case "NOBOLD", "NOB": 71 cb.bold = -1 72 return true 73 74 case "ITALIC", "I": 75 cb.italic = 1 76 return true 77 78 case "NOITALIC", "NOI": 79 cb.italic = -1 80 return true 81 82 case "UNDERLINE", "U": 83 cb.underline = 1 84 return true 85 86 case "NOUNDERLINE", "NOU": 87 cb.underline = -1 88 return true 89 } 90 91 // okay, it's color, but which one? what format? RGB? HEX? RGBA? (lol, wtf?) 92 // TODO: Add supporting of color's literals like "red", "pink", "blue", etc. 93 94 // what's kind of color? default is fg 95 var colorDestination *int16 96 switch { 97 98 case strings.HasPrefix(verbPart, "BG:"): 99 colorDestination = &cb.bg 100 verbPart = strings.TrimSpace(verbPart[3:]) 101 102 case strings.HasPrefix(verbPart, "FG:"): // already defaulted 103 colorDestination = &cb.fg 104 verbPart = strings.TrimSpace(verbPart[3:]) 105 106 default: 107 colorDestination = &cb.fg 108 } 109 110 // handle special easy cases cases 111 switch { 112 case len(verbPart) == 0: 113 // rare case, wasn't more chars after "bg" or "fg" 114 return false 115 116 case verbPart == "-1": 117 // color cleanup (to default in term) 118 *colorDestination = -1 119 return true 120 121 case verbPart[0] == '#': 122 // easy case if it's explicit hex 123 return cb.parseHexTo(verbPart[1:], colorDestination) 124 } 125 126 // maybe default ASCII seq? 127 switch { 128 case strings.HasPrefix(verbPart, "ASCII:"): 129 return cb.parseBaseASCIITo(verbPart[6:], colorDestination) 130 131 case strings.HasPrefix(verbPart, "ASCII(") && verbPart[len(verbPart)-1] == ')': 132 return cb.parseBaseASCIITo(verbPart[6:len(verbPart)-1], colorDestination) 133 } 134 135 // okay, maybe easy rgb/rgba? 136 switch { 137 case strings.HasPrefix(verbPart, "RGB:"): 138 return cb.parseRgbTo(verbPart[4:], colorDestination) 139 140 case strings.HasPrefix(verbPart, "RGBA:"): 141 return cb.parseRgbTo(verbPart[5:], colorDestination) 142 143 case strings.HasPrefix(verbPart, "RGB(") && verbPart[len(verbPart)-1] == ')': 144 return cb.parseRgbTo(verbPart[4:len(verbPart)-1], colorDestination) 145 146 case strings.HasPrefix(verbPart, "RGBA(") && verbPart[len(verbPart)-1] == ')': 147 return cb.parseRgbTo(verbPart[4:len(verbPart)-1], colorDestination) 148 } 149 150 // okay maybe rgb by comma? 151 if commas := strings.Count(verbPart, ","); commas >= 3 && commas <= 4 { 152 return cb.parseRgbTo(verbPart, colorDestination) 153 } 154 155 // believe it's just XTerm 256 colors code 156 return cb.parseX256To(verbPart, colorDestination) 157 } 158 159 func (_ *colorBuilder) parseBaseASCIITo(verbPart string, destination *int16) (parsed bool) { 160 // --- REMINDER! 1ST ARGUMENT IS ALWAYS UPPER CASED! --- 161 162 asciiColor, _ := strconv.Atoi(verbPart) 163 // it's not necessary to check err, cause if error is occurred, asciiColor == 0 164 if (asciiColor >= 30 && asciiColor <= 37) || (asciiColor >= 40 && asciiColor <= 47) || 165 (asciiColor >= 90 && asciiColor <= 97) || (asciiColor >= 100 && asciiColor <= 107) { 166 167 *destination = int16(asciiColor) | (1 << 14) 168 return true 169 } 170 171 return false 172 } 173 174 func (_ *colorBuilder) parseX256To(verbPart string, destination *int16) (parsed bool) { 175 176 xterm256color, err := strconv.Atoi(verbPart) 177 if err != nil || xterm256color < 0 || xterm256color > 255 { 178 return false 179 } 180 181 *destination = int16(xterm256color) 182 return true 183 } 184 185 func (_ *colorBuilder) parseHexTo(verbPart string, destination *int16) (parsed bool) { 186 // --- REMINDER! 1ST ARGUMENT IS ALWAYS UPPER CASED! --- 187 188 switch verbPart = strings.TrimSpace(verbPart); len(verbPart) { 189 case 4: 190 // short case with alpha, ignore alpha, extend to 6 191 verbPart = verbPart[:3] 192 fallthrough 193 case 3: 194 // short case, extend to 6 195 var hexParts [6]uint8 196 hexParts[0], hexParts[1] = verbPart[0], verbPart[0] 197 hexParts[2], hexParts[3] = verbPart[1], verbPart[1] 198 hexParts[4], hexParts[5] = verbPart[2], verbPart[2] 199 verbPart = string(hexParts[:]) 200 case 8: 201 // with alpha, ignore it 202 verbPart = verbPart[:6] 203 case 6: 204 // default HEX case, handle later 205 default: 206 return false 207 } 208 209 termColor, err := xtermcolor.FromHexStr(verbPart) 210 *destination = int16(termColor) 211 212 return err == nil 213 } 214 215 func (_ *colorBuilder) parseRgbTo(verbPart string, destination *int16) (parsed bool) { 216 // --- REMINDER! 1ST ARGUMENT IS ALWAYS UPPER CASED! --- 217 218 rgbParts := strings.Split(strings.TrimSpace(verbPart), ",") 219 if l := len(rgbParts); l < 3 && l > 4 { 220 return false 221 } 222 223 var ( 224 r, g, b int 225 err1, err2, err3 error 226 ) 227 228 r, err1 = strconv.Atoi(strings.TrimSpace(rgbParts[0])) 229 g, err2 = strconv.Atoi(strings.TrimSpace(rgbParts[1])) 230 b, err3 = strconv.Atoi(strings.TrimSpace(rgbParts[2])) 231 232 if err1 != nil || err2 != nil || err3 != nil || 233 r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 { 234 return false 235 } 236 237 rgb := color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255} 238 *destination = int16(xtermcolor.FromColor(rgb)) 239 240 return true 241 } 242 243 func (cb *colorBuilder) encode() string { 244 245 if cb.bg == 100 { 246 return "\033[0m" 247 } 248 249 // TODO: Here is too much Golang string mem reallocations 250 // maybe use []byte instead of string with only one allocation ? 251 252 // https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 253 out := "\033[" 254 255 switch cb.bold { 256 case 1: 257 out += "01;" // enable bold 258 case -1: 259 out += "22;" // disable bold 260 } 261 262 switch cb.italic { 263 case 1: 264 out += "03;" // enable italic 265 case -1: 266 out += "23;" // disable italic 267 } 268 269 switch cb.underline { 270 case 1: 271 out += "04;" // enable underline 272 case -1: 273 out += "24;" // disable underline 274 } 275 276 switch /* FOREGROUND COLOR */ { 277 case cb.fg == -2: 278 // do nothing, use those one that was used 279 case cb.fg == -1: 280 // set foreground to term default 281 out += "39;" 282 case cb.fg&(1<<14) != 0: 283 // first 16 ASCII sys colors 284 cb.fg &^= 1 << 14 285 if cb.fg >= 40 && cb.fg <= 47 || cb.fg >= 100 { 286 cb.fg -= 10 287 } 288 out += strconv.Itoa(int(cb.fg)) + ";" 289 default: 290 out += "38;5;" + strconv.Itoa(int(cb.fg)) + ";" 291 } 292 293 switch /* BACKGROUND COLOR */ { 294 case cb.bg == -2: 295 // do nothing, use those one that was used 296 case cb.bg == -1: 297 // set background to term default 298 out += "49;" 299 case cb.bg&(1<<14) != 0: 300 // first 16 ASCII sys colors 301 cb.bg &^= 1 << 14 302 if cb.bg < 40 || cb.bg >= 90 && cb.bg <= 97 { 303 cb.bg += 10 304 } 305 out += strconv.Itoa(int(cb.fg)) + ";" 306 default: 307 out += "48;5;" + strconv.Itoa(int(cb.bg)) + ";" 308 } 309 310 if out[len(out)-1] != ';' { 311 return "" // all values are default and ignored 312 } 313 314 out = out[:len(out)-1] + "m" 315 return out 316 }