From 23e9c5d58dee17ed0b6294dc96dd40acdc4cbe69 Mon Sep 17 00:00:00 2001 From: Aadhavan Srinivasan Date: Mon, 21 Apr 2025 11:48:56 -0400 Subject: [PATCH] Added SGR fields to the RGB struct for bold and italic; allow RGB values to be -1 (default color); allow underscore in color names --- color.go | 79 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/color.go b/color.go index b697609..06fbb9c 100644 --- a/color.go +++ b/color.go @@ -18,12 +18,17 @@ type color struct { colorObj *colorData.Color } -// A RGB represents a Red, Blue, Green trio of values. Each value is represented as -// an int. +// A RGB represents a Red, Blue, Green trio of values, along with SGR parameters. +// Each value is represented as an int. For info on SGR parameters, see: +// https://en.wikipedia.org/wiki/ANSI_escape_code#Select_Graphic_Rendition_parameters +// If 'red', 'green' and 'blue' are all -1, then the default terminal color is used. +// If some (but not all) of them are -1, an error is thrown. type RGB struct { + sgr1 int red int blue int green int + sgr2 int } // The following is a list of all possible colors, stored in a map. @@ -76,59 +81,79 @@ func newColorMust(colorString string) color { // characters. func isValidColorName(colorName string) bool { for _, ch := range colorName { - if ch > 'Z' || ch < 'A' { + if (ch > 'Z' || ch < 'A') && (ch != '_') { return false } } return true } -// stringToRGB takes a string representing an RGB trio. It constructs and RGB type and +// stringToRGB takes a string representing an RGB five-tuple. It constructs and RGB type and // returns it. Any errors encountered are returned. If an error is returned, it is safe to -// assume that the string doesn't represent an RGB trio. +// assume that the string doesn't represent an RGB five-tuple. func stringToRGB(rgbString string) (*RGB, error) { values := strings.Split(rgbString, " ") // There must be three space-separated strings. - if len(values) != 3 { + if len(values) != 5 { // TODO: Instead of ignoring these errors and returning a generic error (as I do in the // callee), wrap the error returned from this function, inside the error returned by the callee. - return nil, fmt.Errorf("Error parsing RGB trio.") + return nil, fmt.Errorf("Error parsing RGB five-tuple.") } // If any of the strings doesn't represent an integer (or is out of bounds), return an error. // WARNING: LAZY CODE INCOMING var toReturn RGB var err error - toReturn.red, err = strconv.Atoi(values[0]) + toReturn.sgr1, err = strconv.Atoi(values[0]) + if err != nil { + return nil, fmt.Errorf("Error parsing SGR1 integer: Invalid value.") + } + if toReturn.sgr1 < 0 || toReturn.sgr1 > 107 { // Maximum value for SGR values + return nil, fmt.Errorf("Error parsing SGR1 integer: Out-of-bounds.") + } + toReturn.red, err = strconv.Atoi(values[1]) if err != nil { return nil, fmt.Errorf("Error parsing RED integer: Invalid value.") } - if toReturn.red < 0 || toReturn.red > 255 { + if toReturn.red < -1 || toReturn.red > 255 { return nil, fmt.Errorf("Error parsing RED integer: Out-of-bounds.") } - toReturn.blue, err = strconv.Atoi(values[1]) + toReturn.blue, err = strconv.Atoi(values[2]) if err != nil { return nil, fmt.Errorf("Error parsing BLUE integer: Invalid value.") } - if toReturn.blue < 0 || toReturn.blue > 255 { + if toReturn.blue < -1 || toReturn.blue > 255 { return nil, fmt.Errorf("Error parsing BLUE integer: Out-of-bounds.") } - toReturn.green, err = strconv.Atoi(values[2]) + toReturn.green, err = strconv.Atoi(values[3]) if err != nil { return nil, fmt.Errorf("Error parsing GREEN integer: Invalid value.") } - if toReturn.green < 0 || toReturn.green > 255 { + if toReturn.green < -1 || toReturn.green > 255 { return nil, fmt.Errorf("Error parsing GREEN integer: Out-of-bounds.") } + toReturn.sgr2, err = strconv.Atoi(values[4]) + if err != nil { + return nil, fmt.Errorf("Error parsing SGR2 integer: Invalid value.") + } + if toReturn.sgr2 < 0 || toReturn.sgr2 > 107 { + return nil, fmt.Errorf("Error parsing SGR2 integer: Out-of-bounds.") + } + + if !(toReturn.red > 0 && toReturn.blue > 0 && toReturn.green > 0) && + !(toReturn.red == -1 && toReturn.green == -1 && toReturn.blue == -1) { + return nil, fmt.Errorf("Error parsing color: All values must be positive or -1 for default terminal color.") + } return &toReturn, nil } // loadColorsFromFile loads the colors defined in the given config file, and adds them to // the possibleColors map. This allows the user to define custom colors at run-time. // The colors config file has the following syntax: -// COLOR: +// COLOR: // // Note that the color must be capitalized (and not contain spaces), and the R, G and B -// values must be from 0 to 255. +// values must be from -1 to 255 (-1 refers to the default terminal color, and all three values +// must be -1 for this to work). func loadColorsFromFile(filepath string) error { data, err := os.ReadFile(filepath) if err != nil { @@ -151,8 +176,28 @@ func loadColorsFromFile(filepath string) error { // If we haven't returned an error yet, the color must be valid. // Add it to the map. colorData.New() expects values of type colorData.Attribute, // so we must cast our RGB values accordingly. - possibleColors[item.Key.(string)] = color{item.Key.(string), colorData.New(38, 2, colorData.Attribute(rgb.red), colorData.Attribute(rgb.blue), colorData.Attribute(rgb.green))} - + // First, check if one of the color values is -1. If it is, they must all be negative (based + // on the check in 'stringToRGB()'). If this is the case, don't put the color values. + if rgb.red == -1 { + possibleColors[item.Key.(string)] = color{ + item.Key.(string), + colorData.New( + colorData.Attribute(rgb.sgr2), + ), + } + } else { + possibleColors[item.Key.(string)] = color{ + item.Key.(string), + colorData.New( + colorData.Attribute(rgb.sgr1), + 2, + colorData.Attribute(rgb.red), + colorData.Attribute(rgb.blue), + colorData.Attribute(rgb.green), + colorData.Attribute(rgb.sgr2), + ), + } + } } return nil