diff --git a/pkg/utils/color.go b/pkg/utils/color.go index 84d5196d5..18b04db1f 100644 --- a/pkg/utils/color.go +++ b/pkg/utils/color.go @@ -2,12 +2,27 @@ package utils import ( "regexp" + "sync" ) +var decoloriseCache = make(map[string]string) +var decoloriseMutex sync.Mutex + // Decolorise strips a string of color func Decolorise(str string) string { + decoloriseMutex.Lock() + defer decoloriseMutex.Unlock() + + if decoloriseCache[str] != "" { + return decoloriseCache[str] + } + re := regexp.MustCompile(`\x1B\[([0-9]{1,3}(;[0-9]{1,3})*)?[mGK]`) - return re.ReplaceAllString(str, "") + ret := re.ReplaceAllString(str, "") + + decoloriseCache[str] = ret + + return ret } func IsValidHexValue(v string) bool { diff --git a/pkg/utils/formatting.go b/pkg/utils/formatting.go index 55c04c85f..80717bfad 100644 --- a/pkg/utils/formatting.go +++ b/pkg/utils/formatting.go @@ -17,14 +17,48 @@ func WithPadding(str string, padding int) string { } func RenderDisplayStrings(displayStringsArr [][]string) string { + displayStringsArr = excludeBlankColumns(displayStringsArr) padWidths := getPadWidths(displayStringsArr) - paddedDisplayStrings := getPaddedDisplayStrings(displayStringsArr, padWidths) + output := getPaddedDisplayStrings(displayStringsArr, padWidths) - return strings.Join(paddedDisplayStrings, "\n") + return output } -func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string { - paddedDisplayStrings := make([]string, len(stringArrays)) +// NOTE: this mutates the input slice for the sake of performance +func excludeBlankColumns(displayStringsArr [][]string) [][]string { + if len(displayStringsArr) == 0 { + return displayStringsArr + } + + // if all rows share a blank column, we want to remove that column + toRemove := []int{} +outer: + for i := range displayStringsArr[0] { + for _, strings := range displayStringsArr { + if strings[i] != "" { + continue outer + } + } + toRemove = append(toRemove, i) + } + + if len(toRemove) == 0 { + return displayStringsArr + } + + // remove the columns + for i, strings := range displayStringsArr { + for j := len(toRemove) - 1; j >= 0; j-- { + strings = append(strings[:toRemove[j]], strings[toRemove[j]+1:]...) + } + displayStringsArr[i] = strings + } + + return displayStringsArr +} + +func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) string { + builder := strings.Builder{} for i, stringArray := range stringArrays { if len(stringArray) == 0 { continue @@ -33,14 +67,19 @@ func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string if len(stringArray)-1 < j { continue } - paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " " + builder.WriteString(WithPadding(stringArray[j], padWidth)) + builder.WriteString(" ") } if len(stringArray)-1 < len(padWidths) { continue } - paddedDisplayStrings[i] += stringArray[len(padWidths)] + builder.WriteString(stringArray[len(padWidths)]) + + if i < len(stringArrays)-1 { + builder.WriteString("\n") + } } - return paddedDisplayStrings + return builder.String() } func getPadWidths(stringArrays [][]string) []int { diff --git a/pkg/utils/formatting_test.go b/pkg/utils/formatting_test.go index c07f1ed22..5b9393d50 100644 --- a/pkg/utils/formatting_test.go +++ b/pkg/utils/formatting_test.go @@ -37,27 +37,6 @@ func TestWithPadding(t *testing.T) { } } -// TestGetPaddedDisplayStrings is a function. -func TestGetPaddedDisplayStrings(t *testing.T) { - type scenario struct { - stringArrays [][]string - padWidths []int - expected []string - } - - scenarios := []scenario{ - { - [][]string{{"a", "b"}, {"c", "d"}}, - []int{1}, - []string{"a b", "c d"}, - }, - } - - for _, s := range scenarios { - assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths)) - } -} - func TestGetPadWidths(t *testing.T) { type scenario struct { input [][]string @@ -162,3 +141,44 @@ func TestTruncateWithEllipsis(t *testing.T) { assert.EqualValues(t, s.expected, TruncateWithEllipsis(s.str, s.limit)) } } + +func TestRenderDisplayStrings(t *testing.T) { + type scenario struct { + input [][]string + expected string + } + + tests := []scenario{ + { + [][]string{{""}, {""}}, + "", + }, + { + [][]string{{"a"}, {""}}, + "a\n", + }, + { + [][]string{{"a"}, {"b"}}, + "a\nb", + }, + { + [][]string{{"a", "b"}, {"c", "d"}}, + "a b\nc d", + }, + { + [][]string{{"a", "", "c"}, {"d", "", "f"}}, + "a c\nd f", + }, + { + [][]string{{"a", "", "c", ""}, {"d", "", "f", ""}}, + "a c\nd f", + }, + } + + for _, test := range tests { + output := RenderDisplayStrings(test.input) + if !assert.EqualValues(t, output, test.expected) { + t.Errorf("RenderDisplayStrings(%v) = %v, want %v", test.input, output, test.expected) + } + } +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 67debdf3a..c9d64c30e 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -52,6 +52,13 @@ func Min(x, y int) int { return y } +func Max(x, y int) int { + if x > y { + return x + } + return y +} + func AsJson(i interface{}) string { bytes, _ := json.MarshalIndent(i, "", " ") return string(bytes)