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  }