@ -18,12 +18,17 @@ type color struct {
colorObj * colorData . Color
colorObj * colorData . Color
}
}
// A RGB represents a Red, Blue, Green trio of values. Each value is represented as
// A RGB represents a Red, Blue, Green trio of values, along with SGR parameters.
// an int.
// 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 {
type RGB struct {
sgr1 int
red int
red int
blue int
blue int
green int
green int
sgr2 int
}
}
// The following is a list of all possible colors, stored in a map.
// The following is a list of all possible colors, stored in a map.
@ -33,14 +38,14 @@ var possibleColors map[string]color = map[string]color{
"GREEN" : { "GREEN" , colorData . New ( colorData . FgGreen ) } ,
"GREEN" : { "GREEN" , colorData . New ( colorData . FgGreen ) } ,
"YELLOW" : { "YELLOW" , colorData . New ( colorData . FgYellow ) } ,
"YELLOW" : { "YELLOW" , colorData . New ( colorData . FgYellow ) } ,
"BLUE" : { "BLUE" , colorData . New ( colorData . FgBlue ) } ,
"BLUE" : { "BLUE" , colorData . New ( colorData . FgBlue ) } ,
"MAGENTA" : { "MAGENTA" , colorData . New ( 38 , 2 , 254 , 141 , 255 ) } ,
"MAGENTA" : { "MAGENTA" , colorData . New ( colorData . FgMagenta ) } ,
"CYAN" : { "CYAN" , colorData . New ( colorData . FgCyan ) } ,
"CYAN" : { "CYAN" , colorData . New ( colorData . FgCyan ) } ,
"WHITE" : { "WHITE" , colorData . New ( colorData . FgWhite ) } ,
"WHITE" : { "WHITE" , colorData . New ( colorData . FgWhite ) } ,
"GRAY" : { "GRAY" , colorData . New ( colorData . FgWhite , colorData . Faint ) } ,
"GRAY" : { "GRAY" , colorData . New ( colorData . FgWhite , colorData . Faint ) } ,
// Last three numbers are RGB. Reference https://en.wikipedia.org/wiki/ANSI_escape_code for what the first two numbers mean.
// Last three numbers are RGB. Reference https://en.wikipedia.org/wiki/ANSI_escape_code for what the first two numbers mean.
"ORANGE" : { "ORANGE" , colorData . New ( 38 , 2 , 255 , 153 , 28 ) } ,
// "ORANGE": {"ORANGE", colorData.New(38, 2, 255, 153, 28)},
"DARKBLUE" : { "DARKBLUE" , colorData . New ( 38 , 2 , 0 , 112 , 255 ) } ,
// "DARKBLUE": {"DARKBLUE", colorData.New(38, 2, 0, 112, 255)},
"NONE" : { "NONE" , colorData . New ( ) } ,
"NONE" : { "NONE" , colorData . New ( ) } ,
}
}
// Apply the given color 'clr' to all units in 'units', within the indices
// Apply the given color 'clr' to all units in 'units', within the indices
@ -76,63 +81,83 @@ func newColorMust(colorString string) color {
// characters.
// characters.
func isValidColorName ( colorName string ) bool {
func isValidColorName ( colorName string ) bool {
for _ , ch := range colorName {
for _ , ch := range colorName {
if ch > 'Z' || ch < 'A' {
if ( ch > 'Z' || ch < 'A' ) && ( ch != '_' ) {
return false
return false
}
}
}
}
return true
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
// 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 ) {
func stringToRGB ( rgbString string ) ( * RGB , error ) {
values := strings . Split ( rgbString , " " )
values := strings . Split ( rgbString , " " )
// There must be three space-separated strings.
// 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
// 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.
// 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.
// If any of the strings doesn't represent an integer (or is out of bounds), return an error.
// WARNING: LAZY CODE INCOMING
// WARNING: LAZY CODE INCOMING
var toReturn RGB
var toReturn RGB
var err error
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 {
if err != nil {
return nil , fmt . Errorf ( "Error parsing RED integer: Invalid value." )
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." )
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 {
if err != nil {
return nil , fmt . Errorf ( "Error parsing BLUE integer: Invalid value." )
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." )
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 {
if err != nil {
return nil , fmt . Errorf ( "Error parsing GREEN integer: Invalid value." )
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." )
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
return & toReturn , nil
}
}
// loadColorsFromFile loads the colors defined in the given config file, and adds them to
// 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 possibleColors map. This allows the user to define custom colors at run-time.
// The colors config file has the following syntax:
// The colors config file has the following syntax:
// COLOR: <RED> <GREEN> <BLUE>
// COLOR: < SGR1> < RED> <GREEN> <BLUE> <SGR2 >
//
//
// Note that the color must be capitalized (and not contain spaces), and the R, G and B
// 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 {
func loadColorsFromFile ( filepath string ) error {
data , err := os . ReadFile ( filepath )
data , err := os . ReadFile ( filepath )
if err != nil {
if err != nil {
panic ( err )
return err
}
}
// Read color config file into a MapSlice
// Read color config file into a MapSlice
tempMapSlice := yaml . MapSlice { }
tempMapSlice := yaml . MapSlice { }
@ -151,8 +176,28 @@ func loadColorsFromFile(filepath string) error {
// If we haven't returned an error yet, the color must be valid.
// 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,
// Add it to the map. colorData.New() expects values of type colorData.Attribute,
// so we must cast our RGB values accordingly.
// 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
return nil