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