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  // ////////////////////////////////////////////////////////////////////////////////// //