pkg.re/essentialkaos/ek@v12.36.0+incompatible/fmtc/fmtc.go (about) 1 // Package fmtc provides methods similar to fmt for colored output 2 package fmtc 3 4 // ////////////////////////////////////////////////////////////////////////////////// // 5 // // 6 // Copyright (c) 2021 ESSENTIAL KAOS // 7 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 8 // // 9 // ////////////////////////////////////////////////////////////////////////////////// // 10 11 import ( 12 "bytes" 13 "errors" 14 "fmt" 15 "io" 16 "os" 17 "strconv" 18 "strings" 19 ) 20 21 // ////////////////////////////////////////////////////////////////////////////////// // 22 23 const ( 24 _CODE_RESET = "\033[0m" 25 _CODE_CLEAN_LINE = "\033[2K\r" 26 _CODE_BACKSPACE = "\b" 27 _CODE_BELL = "\a" 28 ) 29 30 // ////////////////////////////////////////////////////////////////////////////////// // 31 32 // codes map tag -> escape code 33 var codes = map[rune]int{ 34 // Special 35 '-': -1, // Light colors 36 '!': 0, // Default 37 '*': 1, // Bold 38 '^': 2, // Dim 39 '_': 4, // Underline 40 '~': 5, // Blink 41 '@': 7, // Reverse 42 43 // Text 44 'd': 30, // Black (Dark) 45 'r': 31, // Red 46 'g': 32, // Green 47 'y': 33, // Yellow 48 'b': 34, // Blue 49 'm': 35, // Magenta 50 'c': 36, // Cyan 51 's': 37, // Gray (Smokey) 52 'w': 97, // White 53 54 // Background 55 'D': 40, // Black (Dark) 56 'R': 41, // Red 57 'G': 42, // Green 58 'Y': 43, // Yellow 59 'B': 44, // Blue 60 'M': 45, // Magenta 61 'C': 46, // Cyan 62 'S': 47, // Gray (Smokey) 63 'W': 107, // White 64 } 65 66 // ////////////////////////////////////////////////////////////////////////////////// // 67 68 // DisableColors disables all colors and modificators in output 69 var DisableColors = os.Getenv("NO_COLOR") != "" 70 71 // ////////////////////////////////////////////////////////////////////////////////// // 72 73 // Print formats using the default formats for its operands and writes to standard 74 // output. Spaces are added between operands when neither is a string. It returns 75 // the number of bytes written and any write error encountered. 76 // 77 // Supported color codes: 78 // 79 // Modificators: 80 // - Light colors 81 // ! Default 82 // * Bold 83 // ^ Dim 84 // _ Underline 85 // ~ Blink 86 // @ Reverse 87 // 88 // Foreground colors: 89 // d Black (Dark) 90 // r Red 91 // g Green 92 // y Yellow 93 // b Blue 94 // m Magenta 95 // c Cyan 96 // s Gray (Smokey) 97 // w White 98 // 99 // Background colors: 100 // D Black (Dark) 101 // R Red 102 // G Green 103 // Y Yellow 104 // B Blue 105 // M Magenta 106 // C Cyan 107 // S Gray (Smokey) 108 // W White 109 // 110 // 256 colors: 111 // #code foreground color 112 // %code background color 113 func Print(a ...interface{}) (int, error) { 114 applyColors(&a, -1, DisableColors) 115 return fmt.Print(a...) 116 } 117 118 // Println formats using the default formats for its operands and writes to standard 119 // output. Spaces are always added between operands and a newline is appended. It 120 // returns the number of bytes written and any write error encountered. 121 func Println(a ...interface{}) (int, error) { 122 applyColors(&a, -1, DisableColors) 123 return fmt.Println(a...) 124 } 125 126 // Printf formats according to a format specifier and writes to standard output. It 127 // returns the number of bytes written and any write error encountered. 128 func Printf(f string, a ...interface{}) (int, error) { 129 return fmt.Printf(searchColors(f, -1, DisableColors), a...) 130 } 131 132 // Fprint formats using the default formats for its operands and writes to w. 133 // Spaces are added between operands when neither is a string. It returns the 134 // number of bytes written and any write error encountered. 135 func Fprint(w io.Writer, a ...interface{}) (int, error) { 136 applyColors(&a, -1, DisableColors) 137 return fmt.Fprint(w, a...) 138 } 139 140 // Fprintln formats using the default formats for its operands and writes to w. 141 // Spaces are always added between operands and a newline is appended. It returns 142 // the number of bytes written and any write error encountered. 143 func Fprintln(w io.Writer, a ...interface{}) (int, error) { 144 applyColors(&a, -1, DisableColors) 145 return fmt.Fprintln(w, a...) 146 } 147 148 // Fprintf formats according to a format specifier and writes to w. It returns 149 // the number of bytes written and any write error encountered. 150 func Fprintf(w io.Writer, f string, a ...interface{}) (int, error) { 151 return fmt.Fprintf(w, searchColors(f, -1, DisableColors), a...) 152 } 153 154 // Sprint formats using the default formats for its operands and returns the 155 // resulting string. Spaces are added between operands when neither is a string. 156 func Sprint(a ...interface{}) string { 157 applyColors(&a, -1, DisableColors) 158 return fmt.Sprint(a...) 159 } 160 161 // Sprintf formats according to a format specifier and returns the resulting 162 // string. 163 func Sprintf(f string, a ...interface{}) string { 164 return fmt.Sprintf(searchColors(f, -1, DisableColors), a...) 165 } 166 167 // Sprintln formats using the default formats for its operands and returns the 168 // resulting string. Spaces are always added between operands and a newline is 169 // appended. 170 func Sprintln(a ...interface{}) string { 171 applyColors(&a, -1, DisableColors) 172 return fmt.Sprintln(a...) 173 } 174 175 // Errorf formats according to a format specifier and returns the string as a 176 // value that satisfies error. 177 func Errorf(f string, a ...interface{}) error { 178 return errors.New(Sprintf(f, a...)) 179 } 180 181 // TPrintf removes all content on the current line and prints the new message 182 func TPrintf(f string, a ...interface{}) (int, error) { 183 fmt.Printf(_CODE_CLEAN_LINE) 184 return Printf(f, a...) 185 } 186 187 // TPrintln removes all content on the current line and prints the new message 188 // with a new line symbol at the end 189 func TPrintln(a ...interface{}) (int, error) { 190 fmt.Printf(_CODE_CLEAN_LINE) 191 return Println(a...) 192 } 193 194 // LPrintf formats according to a format specifier and writes to standard output 195 // limited by the text size 196 func LPrintf(maxSize int, f string, a ...interface{}) (int, error) { 197 s := fmt.Sprintf(f, a...) 198 return fmt.Printf(searchColors(s, maxSize, DisableColors)) 199 } 200 201 // LPrintln formats using the default formats for its operands and writes to standard 202 // output limited by the text size 203 func LPrintln(maxSize int, a ...interface{}) (int, error) { 204 applyColors(&a, maxSize, DisableColors) 205 return fmt.Println(a...) 206 } 207 208 // TLPrintf removes all content on the current line and prints the new message 209 // limited by the text size 210 func TLPrintf(maxSize int, f string, a ...interface{}) (int, error) { 211 fmt.Printf(_CODE_CLEAN_LINE) 212 return LPrintf(maxSize, f, a...) 213 } 214 215 // TPrintln removes all content on the current line and prints the new message 216 // limited by the text size with a new line symbol at the end 217 func TLPrintln(maxSize int, a ...interface{}) (int, error) { 218 fmt.Printf(_CODE_CLEAN_LINE) 219 return LPrintln(maxSize, a...) 220 } 221 222 // NewLine prints a newline to standard output 223 func NewLine() (int, error) { 224 return fmt.Println("") 225 } 226 227 // Clean returns string without color tags 228 func Clean(s string) string { 229 return searchColors(s, -1, true) 230 } 231 232 // Bell prints alert (bell) symbol 233 func Bell() { 234 fmt.Printf(_CODE_BELL) 235 } 236 237 // Is256ColorsSupported returns true if 256 colors is supported 238 func Is256ColorsSupported() bool { 239 return strings.Contains(os.Getenv("TERM"), "256color") 240 } 241 242 // ////////////////////////////////////////////////////////////////////////////////// // 243 244 // codebeat:disable[LOC,BLOCK_NESTING] 245 246 func tag2ANSI(tag string, clean bool) string { 247 if clean { 248 return "" 249 } 250 251 if isExtendedColorTag(tag) { 252 return parseExtendedColor(tag) 253 } 254 255 light := strings.Contains(tag, "-") 256 reset := strings.Contains(tag, "!") 257 258 var chars string 259 260 for _, key := range tag { 261 code := codes[key] 262 263 switch { 264 case light && code == 37: // Light gray = Dark gray 265 chars += "90;" 266 continue 267 case light && code == 97: // Light gray = Dark gray 268 chars += "97;" 269 continue 270 } 271 272 switch key { 273 case '-', '!': 274 continue 275 276 case '*', '^', '_', '~', '@': 277 if reset { 278 chars += getResetCode(code) 279 } else { 280 chars += strconv.Itoa(code) 281 } 282 283 case 'D', 'R', 'G', 'Y', 'B', 'M', 'C', 'S', 'W': 284 chars += strconv.Itoa(code) 285 286 case 'd', 'r', 'g', 'y', 'b', 'm', 'c', 's', 'w': 287 if light { 288 chars += strconv.Itoa(code + 60) 289 } else { 290 chars += strconv.Itoa(code) 291 } 292 } 293 294 chars += ";" 295 } 296 297 if chars == "" { 298 return "" 299 } 300 301 return fmt.Sprintf("\033[" + chars[:len(chars)-1] + "m") 302 } 303 304 // codebeat:enable[LOC,BLOCK_NESTING] 305 306 func parseExtendedColor(tag string) string { 307 // Foreground 308 if strings.HasPrefix(tag, "#") { 309 return "\033[38;5;" + tag[1:] + "m" 310 } 311 312 // Background 313 return "\033[48;5;" + tag[1:] + "m" 314 } 315 316 func getResetCode(code int) string { 317 if code == codes['*'] { 318 code++ 319 } 320 321 return "2" + strconv.Itoa(code) 322 } 323 324 func replaceColorTags(input, output *bytes.Buffer, clean bool) bool { 325 tag := bytes.NewBufferString("") 326 327 LOOP: 328 for { 329 i, _, err := input.ReadRune() 330 331 if err != nil { 332 output.WriteString("{" + tag.String()) 333 return true 334 } 335 336 switch i { 337 default: 338 tag.WriteRune(i) 339 case '{': 340 output.WriteString("{" + tag.String()) 341 tag = bytes.NewBufferString("") 342 case '}': 343 break LOOP 344 } 345 } 346 347 tagStr := tag.String() 348 349 if !isValidTag(tagStr) { 350 output.WriteString("{" + tagStr + "}") 351 return true 352 } 353 354 if tagStr == "!" { 355 if !clean { 356 output.WriteString(_CODE_RESET) 357 } 358 359 return true 360 } 361 362 output.WriteString(tag2ANSI(tagStr, clean)) 363 364 return false 365 } 366 367 func searchColors(text string, limit int, clean bool) string { 368 if text == "" { 369 return "" 370 } 371 372 closed := true 373 counter := 0 374 input := bytes.NewBufferString(text) 375 output := bytes.NewBufferString("") 376 377 for { 378 i, _, err := input.ReadRune() 379 380 if err != nil { 381 break 382 } 383 384 switch i { 385 case '{': 386 closed = replaceColorTags(input, output, clean) 387 case rune(65533): 388 continue 389 default: 390 output.WriteRune(i) 391 counter++ 392 } 393 394 if counter == limit { 395 break 396 } 397 } 398 399 if !closed { 400 output.WriteString(_CODE_RESET) 401 } 402 403 return output.String() 404 } 405 406 func applyColors(a *[]interface{}, limit int, clean bool) { 407 for i, x := range *a { 408 if s, ok := x.(string); ok { 409 (*a)[i] = searchColors(s, limit, clean) 410 } 411 } 412 } 413 414 func isValidTag(tag string) bool { 415 switch { 416 case tag == "", 417 strings.Trim(tag, "-") == "", 418 strings.Count(tag, "!") > 1, 419 strings.Contains(tag, "!") && strings.Contains(tag, "-"): 420 return false 421 } 422 423 if isValidExtendedTag(tag) { 424 return true 425 } 426 427 for _, r := range tag { 428 _, hasCode := codes[r] 429 430 if !hasCode { 431 return false 432 } 433 } 434 435 return true 436 } 437 438 func isExtendedColorTag(tag string) bool { 439 return strings.HasPrefix(tag, "#") || strings.HasPrefix(tag, "%") 440 } 441 442 func isValidExtendedTag(tag string) bool { 443 if !isExtendedColorTag(tag) { 444 return false 445 } 446 447 tag = strings.TrimLeft(tag, "#%") 448 color, err := strconv.Atoi(tag) 449 450 if err != nil || color < 0 || color > 256 { 451 return false 452 } 453 454 return true 455 } 456 457 // ////////////////////////////////////////////////////////////////////////////////// //