From cea93fb11c587c0cb914700e14a97d775b673e92 Mon Sep 17 00:00:00 2001 From: NimbleArchitect <56797391+NimbleArchitect@users.noreply.github.com> Date: Tue, 7 Feb 2023 12:28:24 +0000 Subject: [PATCH] added environment variable ICE_COLOR updated colours to support custom colours tidied up colour code updated readme --- README.md | 2 +- pkg/plugin/plugin.go | 27 +++++++---- pkg/plugin/status.go | 19 ++++---- pkg/plugin/table.go | 70 +++++++++++++++++++-------- pkg/plugin/utils.go | 111 ++++++++++++++++++++++++++++++++++++++----- 5 files changed, 181 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index cb6d281..ce236ef 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ ice also supports all the standard kubectl flags in addition to: Flags: -A, --all-namespaces List containers from pods in all namespaces --annotation string Show the selected annotation as a column - --color string Colour columns in the table output. string can be one of: columns, errors, mix, none + --color string Add some much needed colour to the table output. string can be one of: columns, custom, errors, mix and none (overrides environment variable ICE_COLOUR) -c, --container string Container name. If set shows only the named containers --context string The name of the kubeconfig context to use -m, --match string Filters out results, comma seperated list of COLUMN OP VALUE, where OP can be one of ==,<,>,<=,>= and != diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 2927b7d..9b5a08f 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -34,14 +34,16 @@ type commonFlags struct { annotationPodName string showColumnByName string // list of column names to show, overrides other hidden columns outputAsColour int // which coloring type do we use when displaying columns + useTheseColours [][2]int } const ( - COLOUR_NONE = 0 - COLOUR_ERRORS = 1 - COLOUR_COLUMNS = 2 - COLOUR_MIX = 3 - COLOUR_CUSTOM = 4 + COLOUR_NONE = 0 + COLOUR_ERRORS = 1 + COLOUR_COLUMNS = 2 + COLOUR_MIX = 3 + COLOUR_CUSTOM = 4 + COLOUR_CUSTOMMIX = 5 ) func InitSubCommands(rootCmd *cobra.Command) { @@ -390,7 +392,7 @@ func addCommonFlags(cmdObj *cobra.Command) { cmdObj.Flags().StringP("annotation", "", "", `Show the selected annotation as a column`) cmdObj.Flags().StringP("filename", "f", "", `read pod information from this yaml file instead`) cmdObj.Flags().StringP("columns", "", "", `list of column names to show in the table output, all other columns are hidden`) - cmdObj.Flags().StringP("color", "", "", `Colour columns in the table output. string can be one of: columns, errors, mix, none`) + cmdObj.Flags().StringP("color", "", "", `Add some much needed colour to the table output. string can be one of: columns, custom, errors, mix and none (overrides env variable ICE_COLOUR)`) } func processCommonFlags(cmd *cobra.Command) (commonFlags, error) { @@ -562,7 +564,10 @@ func processCommonFlags(cmd *cobra.Command) (commonFlags, error) { if len(colourOut) > 0 { // we use a switch to match --colour flag so I can expand in future - switch strings.ToLower(colourOut) { + colourEnv := strings.ToLower(colourOut) + colourSet := strings.Split(colourEnv, ";") + + switch strings.ToLower(colourSet[0]) { case "mix": f.outputAsColour = COLOUR_MIX case "columns": @@ -571,9 +576,15 @@ func processCommonFlags(cmd *cobra.Command) (commonFlags, error) { f.outputAsColour = COLOUR_ERRORS case "none": f.outputAsColour = COLOUR_NONE + case "custom": + // f.outputAsColour = COLOUR_CUSTOM + f.useTheseColours, f.outputAsColour, err = getColourSetFromString(colourSet[1:]) + if err != nil { + return commonFlags{}, err + } default: - return commonFlags{}, errors.New("unknown colour type only columns, errors, mix and none are supported") + return commonFlags{}, errors.New("unknown colour type only columns, custom, errors, mix and none are supported") } } diff --git a/pkg/plugin/status.go b/pkg/plugin/status.go index c62a840..4761711 100644 --- a/pkg/plugin/status.go +++ b/pkg/plugin/status.go @@ -94,6 +94,7 @@ func Status(cmd *cobra.Command, kubeFlags *genericclioptions.ConfigFlags, args [ table := Table{} table.ColourOutput = commonFlagList.outputAsColour + table.CustomColours = commonFlagList.useTheseColours builder.Table = &table log.Debug("commonFlagList.showTreeView =", commonFlagList.showTreeView) @@ -203,20 +204,20 @@ func (s *status) BuildBranch(info BuilderInformation, rows [][]Cell) ([]Cell, er // rowOut[10] // message rowOut[0].text = "true" - rowOut[0].colour = [2]int{colourOk, colourModOk} + rowOut[0].colour = colourOk rowOut[1].text = "true" - rowOut[1].colour = [2]int{colourOk, colourModOk} + rowOut[1].colour = colourOk // loop through each row in podTotals and add the columns in each row for _, r := range rows { if r[0].text == "false" { // ready = false rowOut[0].text = "false" // ready - rowOut[0].colour = [2]int{colourBad, colourModBad} + rowOut[0].colour = colourBad } if r[1].text == "false" { rowOut[1].text = "false" // started - rowOut[1].colour = [2]int{colourBad, colourModBad} + rowOut[1].colour = colourBad } rowOut[2].number += r[2].number // restarts @@ -232,7 +233,7 @@ func (s *status) BuildBranch(info BuilderInformation, rows [][]Cell) ([]Cell, er rowOut[3].text = string(info.Data.pod.Status.Phase) // state } else { rowOut[3].text = "Terminating" // state - rowOut[3].colour = [2]int{colourWarn, colourModWarn} + rowOut[3].colour = colourWarn } rowOut[4].text = info.Data.pod.Status.Reason // reason rowOut[8].text = info.Data.pod.CreationTimestamp.Format(timestampFormat) // timestamp @@ -277,7 +278,7 @@ func (s *status) BuildContainerStatus(container v1.ContainerStatus, info Builder message = state.Waiting.Message // waiting state dosent have a start time so we skip setting the age variable, used further down skipAgeCalculation = true - colourcode = [2]int{colourWarn, colourModWarn} + colourcode = colourWarn } if state.Terminated != nil { @@ -292,9 +293,9 @@ func (s *status) BuildContainerStatus(container v1.ContainerStatus, info Builder message = state.Terminated.Message if rawExitCode == 0 { - colourcode = [2]int{colourOk, colourModOk} + colourcode = colourOk } else { - colourcode = [2]int{colourBad, colourModBad} + colourcode = colourBad } } @@ -302,7 +303,7 @@ func (s *status) BuildContainerStatus(container v1.ContainerStatus, info Builder strState = "Running" startedAt = state.Running.StartedAt.Format(timestampFormat) startTime = state.Running.StartedAt.Time - colourcode = [2]int{colourOk, colourModOk} + colourcode = colourOk } if container.Started != nil { diff --git a/pkg/plugin/table.go b/pkg/plugin/table.go index 059f6c8..7969933 100644 --- a/pkg/plugin/table.go +++ b/pkg/plugin/table.go @@ -39,6 +39,7 @@ type Table struct { placeHolder map[int][]Cell placeHolderID int ColourOutput int + CustomColours [][2]int } // SetHeader sets the header row to the specified array of strings @@ -178,18 +179,41 @@ func (t *Table) Print() { var cellcolour [2]int var withColour bool var visibleColumns int + headLine := "" colourArray := make([][2]int, t.headCount) - if t.ColourOutput != COLOUR_NONE { + switch t.ColourOutput { + case COLOUR_NONE: + withColour = false + case COLOUR_CUSTOMMIX: + fallthrough + case COLOUR_CUSTOM: + withColour = true + maxColours := len(t.CustomColours) + for i := 0; i < t.headCount; i++ { + colourCode := int(math.Mod(float64(i), float64(maxColours))) + colourArray[i][0] = t.CustomColours[colourCode][0] // colour + colourArray[i][1] = t.CustomColours[colourCode][1] // colour modifier + } + default: withColour = true - maxColours := 7 + // generate the colour numbers for the default colour wheel, the colour set is repeated if there are more heades than colours + maxColours := 14 modFlip := 0 for i := 0; i < t.headCount; i++ { colourCode := int(math.Mod(float64(i), float64(maxColours))) - colourArray[i][0] = colourCode + 30 + if colourCode < 6 { + // we start at 31 and increase for 6 colours this allow us to excclude black and light gray + colourArray[i][0] = colourCode + 31 + } else { + // the second set covers the dark variations of the colours + colourArray[i][0] = colourCode + 84 + } + + // we flip the text to bold after every colour run colourArray[i][1] = modFlip if colourCode >= maxColours-1 { @@ -219,7 +243,7 @@ func (t *Table) Print() { word = "-" } - if t.ColourOutput == COLOUR_MIX || t.ColourOutput == COLOUR_COLUMNS { + if t.ColourOutput != COLOUR_NONE && t.ColourOutput != COLOUR_ERRORS { word = fmt.Sprintf("\033[%d;%dm%s%s", cellcolour[1], cellcolour[0], word, colourEnd) } pad := strings.Repeat(" ", t.head[idx].columnLength-runelen) @@ -257,7 +281,26 @@ func (t *Table) Print() { continue } - cellcolour := colourArray[visibleColumns] + if withColour { // if colour wanted + cellcolour = colourArray[visibleColumns] //set colour from wheel as default colour + switch t.ColourOutput { + case COLOUR_ERRORS: + // override if we should only show error colours + if cell.colour[0] != -1 { + cellcolour = cell.colour + } else { + cellcolour[0] = -1 + } + case COLOUR_CUSTOMMIX: + fallthrough + case COLOUR_MIX: + // override if we should mix colours + if cell.colour[0] > 0 { + cellcolour = cell.colour + } + } + } + visibleColumns += 1 if len(cell.text) == 0 { @@ -273,20 +316,9 @@ func (t *Table) Print() { pad := strings.Repeat(" ", spaceCount) // colour output has been set and the cell has data - if withColour { - if t.ColourOutput == COLOUR_MIX || t.ColourOutput == COLOUR_COLUMNS { - celltxt = fmt.Sprintf("\033[%d;%dm%s%s", cellcolour[1], cellcolour[0], origtxt, colourEnd) - } - - // we check for errors last so it can overwrite the column colours when we are using the mix colour set - if cell.colour[0] > -1 && (t.ColourOutput == COLOUR_ERRORS || t.ColourOutput == COLOUR_MIX) { - // error colour set uses red/yellow/green for ok/warning/problem - if cell.colour[0] == 0 && t.ColourOutput == COLOUR_MIX { - celltxt = fmt.Sprintf("\033[%d;%dm%s%s", cellcolour[1], cellcolour[0], origtxt, colourEnd) - } else { - celltxt = fmt.Sprintf("\033[%d;%dm%s%s", cell.colour[1], cell.colour[0], origtxt, colourEnd) - } - } + if withColour && cellcolour[0] != -1 { + // so we add the colour codes and modifier + celltxt = fmt.Sprintf("\033[%d;%dm%s%s", cellcolour[1], cellcolour[0], origtxt, colourEnd) } line += fmt.Sprint(celltxt, pad) diff --git a/pkg/plugin/utils.go b/pkg/plugin/utils.go index a1cc850..1938310 100644 --- a/pkg/plugin/utils.go +++ b/pkg/plugin/utils.go @@ -1,8 +1,10 @@ package plugin import ( + "errors" "fmt" "math" + "strconv" "strings" "k8s.io/apimachinery/pkg/util/intstr" @@ -10,12 +12,11 @@ import ( const colourEnd = "\033[0m" const colourNone = -1 -const colourBad = 31 -const colourModBad = 0 -const colourOk = 32 -const colourModOk = 0 -const colourWarn = 33 -const colourModWarn = 0 + +// [0] = colour, [1] = modifier // bold,flashing,underline, etc +var colourBad = [2]int{31, 0} +var colourOk = [2]int{32, 0} +var colourWarn = [2]int{33, 0} // always returns false if the flagList.container is empty as we expect to show all containers // returns true if we dont have a match @@ -137,27 +138,115 @@ func portAsString(port intstr.IntOrString) string { return "" } +// setColourValue set the colour by value, currently 0-74=good, 75-89=warning, 76-100=bad func setColourValue(value int) [2]int { var colour [2]int - colour = [2]int{colourOk, colourModOk} + colour = colourOk if value > 90 { - colour = [2]int{colourBad, colourModBad} + colour = colourBad } else if value > 75 { - colour = [2]int{colourWarn, colourModWarn} + colour = colourWarn } return colour } +// setColourBoolean set the colour form bool currently: true=good, false=bad func setColourBoolean(value bool) [2]int { var colour [2]int if value { - colour = [2]int{colourOk, colourModOk} + colour = colourOk } else { - colour = [2]int{colourBad, colourModBad} + colour = colourBad } return colour } + +// splitColourString decodes a given colour string item (0.0 or x0.0) into its component parts +// +// returns state srting (g, w, b) if found, colour, modifier and error state +func splitColourString(colour string) (string, int, int, error) { + var colourCode int + var colourMod int + + log := logger{location: "splitColourString"} + log.Debug("Start") + + rawColour := colour + rawColourArray := strings.Split(rawColour, "") + + prefixChar := rawColourArray[0] + + _, err := strconv.Atoi(prefixChar) + if err == nil { + // we only have a number.number to deal with + prefixChar = "" + } else { + colourArray := rawColourArray[1:len(rawColourArray)] + rawColour = strings.Join(colourArray, "") + } + + // we only have a number.number to deal with + rawColourString := strings.Split(rawColour, ".") + + colourMod, err = strconv.Atoi(rawColourString[0]) + if err != nil { + return "", 0, 0, errors.New("invalid custom colour modifier") + } + colourCode, err = strconv.Atoi(rawColourString[1]) + if err != nil { + return "", 0, 0, errors.New("invalid custom colour") + } + + return prefixChar, colourCode, colourMod, nil +} + +// getColourSetFromString splits the colour string into colour parts, seperated by ; +// +// the colours for good, warning and bad are also set when found +// colourset is set to COLOUR_CUSTOM by default, if g, w or b is found in the colour then COLOUR_CUSTOMMIX is returned instead +// returns the colours as an array[x][2], colourset and error state +func getColourSetFromString(colours []string) ([][2]int, int, error) { + var colourArray [][2]int + colourset := COLOUR_CUSTOM + + log := logger{location: "getColourSetFromString"} + log.Debug("Start") + + for _, v := range colours { + if len(v) == 0 { + continue + } + if len(v) <= 3 { + return [][2]int{}, COLOUR_NONE, errors.New("invalid custom colour detected") + } + + prefix, code, mod, err := splitColourString(v) + if err != nil { + return [][2]int{}, COLOUR_NONE, err + } + + switch prefix { + case "g": // good colour code + colourOk = [2]int{code, mod} + colourset = COLOUR_CUSTOMMIX + case "w": // warning colour code + colourWarn = [2]int{code, mod} + colourset = COLOUR_CUSTOMMIX + case "b": // bad colour code + colourBad = [2]int{code, mod} + colourset = COLOUR_CUSTOMMIX + case "": + colourCode := [2]int{code, mod} + colourArray = append(colourArray, colourCode) + } + } + + if len(colourArray) == 0 { + colourArray = append(colourArray, [2]int{-1, 0}) + } + return colourArray, colourset, nil +}