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