diff --git a/internal/tableprinter/table_printer.go b/internal/tableprinter/table_printer.go index 69b22be12..47128afb4 100644 --- a/internal/tableprinter/table_printer.go +++ b/internal/tableprinter/table_printer.go @@ -73,7 +73,7 @@ func NewWithWriter(w io.Writer, isTTY bool, maxWidth int, cs *iostreams.ColorSch // was not padded. In tests cs.Enabled() is false which allows us to avoid having to fix up // numerous tests that verify header padding. var paddingFunc func(int, string) string - if cs.Enabled() { + if cs.Enabled { paddingFunc = text.PadRight } diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go index 8fbf4d6c9..14351418f 100644 --- a/pkg/cmd/gist/list/list_test.go +++ b/pkg/cmd/gist/list/list_test.go @@ -654,50 +654,57 @@ func Test_highlightMatch(t *testing.T) { tests := []struct { name string input string - color bool + cs *iostreams.ColorScheme want string }{ { name: "single match", input: "Octo", + cs: &iostreams.ColorScheme{}, want: "Octo", }, { name: "single match (color)", input: "Octo", - color: true, - want: "\x1b[0;30;43mOcto\x1b[0m", + cs: &iostreams.ColorScheme{ + Enabled: true, + }, + want: "\x1b[0;30;43mOcto\x1b[0m", }, { name: "single match with extra", input: "Hello, Octocat!", + cs: &iostreams.ColorScheme{}, want: "Hello, Octocat!", }, { name: "single match with extra (color)", input: "Hello, Octocat!", - color: true, - want: "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m", + cs: &iostreams.ColorScheme{ + Enabled: true, + }, + want: "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m", }, { name: "multiple matches", input: "Octocat/octo", + cs: &iostreams.ColorScheme{}, want: "Octocat/octo", }, { name: "multiple matches (color)", input: "Octocat/octo", - color: true, - want: "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m", + cs: &iostreams.ColorScheme{ + Enabled: true, + }, + want: "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cs := iostreams.NewColorScheme(tt.color, false, false, false, false, iostreams.NoTheme) - matched := false - got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight) + got, err := highlightMatch(tt.input, regex, &matched, tt.cs.Blue, tt.cs.Highlight) assert.NoError(t, err) assert.True(t, matched) assert.Equal(t, tt.want, got) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 92b94c360..86512df4e 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -41,35 +41,24 @@ var ( } ) -// NewColorScheme initializes color logic based on provided terminal capabilities. -// Logic dealing with terminal theme detected, such as whether color is enabled, 8-bit color supported, true color supported, -// labels are colored, and terminal theme detected. -func NewColorScheme(enabled, is256enabled, trueColor, accessibleColors, colorLabels bool, theme string) *ColorScheme { - return &ColorScheme{ - enabled: enabled, - is256enabled: is256enabled, - hasTrueColor: trueColor, - accessibleColors: accessibleColors, - colorLabels: colorLabels, - theme: theme, - } -} - +// ColorScheme controls how text is colored based upon terminal capabilities and user preferences. type ColorScheme struct { - enabled bool - is256enabled bool - hasTrueColor bool - accessibleColors bool - colorLabels bool - theme string -} - -func (c *ColorScheme) Enabled() bool { - return c.enabled + // Enabled is whether color is used at all. + Enabled bool + // EightBitColor is whether the terminal supports 8-bit, 256 colors. + EightBitColor bool + // TrueColor is whether the terminal supports 24-bit, 16 million colors. + TrueColor bool + // Accessible is whether colors must be base 16 colors that users can customize in terminal preferences. + Accessible bool + // ColorLabels is whether labels are colored based on their truecolor RGB hex color. + ColorLabels bool + // Theme is the terminal background color theme used to contextually color text for light, dark, or none at all. + Theme string } func (c *ColorScheme) Bold(t string) string { - if !c.enabled { + if !c.Enabled { return t } return bold(t) @@ -81,16 +70,16 @@ func (c *ColorScheme) Boldf(t string, args ...interface{}) string { func (c *ColorScheme) Muted(t string) string { // Fallback to previous logic if accessible colors preview is disabled. - if !c.accessibleColors { + if !c.Accessible { return c.Gray(t) } // Muted text is only stylized if color is enabled. - if !c.enabled { + if !c.Enabled { return t } - switch c.theme { + switch c.Theme { case LightTheme: return lightThemeMuted(t) case DarkTheme: @@ -105,7 +94,7 @@ func (c *ColorScheme) Mutedf(t string, args ...interface{}) string { } func (c *ColorScheme) Red(t string) string { - if !c.enabled { + if !c.Enabled { return t } return red(t) @@ -116,7 +105,7 @@ func (c *ColorScheme) Redf(t string, args ...interface{}) string { } func (c *ColorScheme) Yellow(t string) string { - if !c.enabled { + if !c.Enabled { return t } return yellow(t) @@ -127,7 +116,7 @@ func (c *ColorScheme) Yellowf(t string, args ...interface{}) string { } func (c *ColorScheme) Green(t string) string { - if !c.enabled { + if !c.Enabled { return t } return green(t) @@ -138,7 +127,7 @@ func (c *ColorScheme) Greenf(t string, args ...interface{}) string { } func (c *ColorScheme) GreenBold(t string) string { - if !c.enabled { + if !c.Enabled { return t } return greenBold(t) @@ -146,10 +135,10 @@ func (c *ColorScheme) GreenBold(t string) string { // Use Muted instead for thematically contrasting color. func (c *ColorScheme) Gray(t string) string { - if !c.enabled { + if !c.Enabled { return t } - if c.is256enabled { + if c.EightBitColor { return gray256(t) } return gray(t) @@ -161,7 +150,7 @@ func (c *ColorScheme) Grayf(t string, args ...interface{}) string { } func (c *ColorScheme) Magenta(t string) string { - if !c.enabled { + if !c.Enabled { return t } return magenta(t) @@ -172,7 +161,7 @@ func (c *ColorScheme) Magentaf(t string, args ...interface{}) string { } func (c *ColorScheme) Cyan(t string) string { - if !c.enabled { + if !c.Enabled { return t } return cyan(t) @@ -183,14 +172,14 @@ func (c *ColorScheme) Cyanf(t string, args ...interface{}) string { } func (c *ColorScheme) CyanBold(t string) string { - if !c.enabled { + if !c.Enabled { return t } return cyanBold(t) } func (c *ColorScheme) Blue(t string) string { - if !c.enabled { + if !c.Enabled { return t } return blue(t) @@ -221,7 +210,7 @@ func (c *ColorScheme) FailureIconWithColor(colo func(string) string) string { } func (c *ColorScheme) HighlightStart() string { - if !c.enabled { + if !c.Enabled { return "" } @@ -229,7 +218,7 @@ func (c *ColorScheme) HighlightStart() string { } func (c *ColorScheme) Highlight(t string) string { - if !c.enabled { + if !c.Enabled { return t } @@ -237,7 +226,7 @@ func (c *ColorScheme) Highlight(t string) string { } func (c *ColorScheme) Reset() string { - if !c.enabled { + if !c.Enabled { return "" } @@ -275,7 +264,7 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string { // Label stylizes text based on label's RGB hex color. func (c *ColorScheme) Label(hex string, x string) string { - if !c.enabled || !c.hasTrueColor || !c.colorLabels || len(hex) != 6 { + if !c.Enabled || !c.TrueColor || !c.ColorLabels || len(hex) != 6 { return x } @@ -287,11 +276,11 @@ func (c *ColorScheme) Label(hex string, x string) string { func (c *ColorScheme) TableHeader(t string) string { // Table headers are only stylized if color is enabled including underline modifier. - if !c.enabled { + if !c.Enabled { return t } - switch c.theme { + switch c.Theme { case DarkTheme: return darkThemeTableHeader(t) case LightTheme: diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index 2adacd63f..f6a72e2a7 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -20,35 +20,52 @@ func TestLabel(t *testing.T) { hex: "fc0303", text: "red", wants: "\033[38;2;252;3;3mred\033[0m", - cs: NewColorScheme(true, true, true, false, true, NoTheme), + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + ColorLabels: true, + }, }, { name: "no truecolor", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, false, false, true, NoTheme), + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + ColorLabels: true, + }, }, { name: "no color", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, false, true, NoTheme), + cs: &ColorScheme{ + ColorLabels: true, + }, }, { name: "invalid hex", hex: "fc0", text: "red", wants: "red", - cs: NewColorScheme(false, false, false, false, true, NoTheme), + cs: &ColorScheme{ + ColorLabels: true, + }, }, { name: "no color labels", hex: "fc0303", text: "red", wants: "red", - cs: NewColorScheme(true, true, true, false, false, NoTheme), + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + ColorLabels: true, + }, }, } @@ -71,62 +88,110 @@ func TestTableHeader(t *testing.T) { expected string }{ { - name: "when color is disabled, text is not stylized", - cs: NewColorScheme(false, false, false, true, false, NoTheme), + name: "when color is disabled, text is not stylized", + cs: &ColorScheme{ + Accessible: true, + Theme: NoTheme, + }, input: "this should not be stylized", expected: "this should not be stylized", }, { - name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, false, false, true, false, NoTheme), + name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: &ColorScheme{ + Enabled: true, + Accessible: true, + Theme: NoTheme, + }, input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { - name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, false, false, true, false, LightTheme), + name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: &ColorScheme{ + Enabled: true, + Accessible: true, + Theme: LightTheme, + }, input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { - name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, false, false, true, false, DarkTheme), + name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used", + cs: &ColorScheme{ + Enabled: true, + Accessible: true, + Theme: DarkTheme, + }, input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, { - name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, true, false, true, false, NoTheme), + name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + Accessible: true, + Theme: NoTheme, + }, input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { - name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, true, false, true, false, LightTheme), + name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + Accessible: true, + Theme: LightTheme, + }, input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { - name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, true, false, true, false, DarkTheme), + name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + Accessible: true, + Theme: DarkTheme, + }, input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, { - name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used", - cs: NewColorScheme(true, true, true, true, false, NoTheme), + name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: NoTheme, + }, input: "this should have no explicit color but underlined", expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset), }, { - name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used", - cs: NewColorScheme(true, true, true, true, false, LightTheme), + name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: LightTheme, + }, input: "this should have dark foreground color and underlined", expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset), }, { - name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used", - cs: NewColorScheme(true, true, true, true, false, DarkTheme), + name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: DarkTheme, + }, input: "this should have light foreground color and underlined", expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset), }, @@ -154,43 +219,70 @@ func TestMuted(t *testing.T) { }{ { name: "when color is disabled but accessible colors are disabled, text is not stylized", - cs: NewColorScheme(false, false, false, false, false, NoTheme), + cs: &ColorScheme{}, input: "this should not be stylized", expected: "this should not be stylized", }, { - name: "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used", - cs: NewColorScheme(true, false, false, false, false, NoTheme), + name: "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used", + cs: &ColorScheme{ + Enabled: true, + }, input: "this should be 4-bit gray", expected: fmt.Sprintf("%sthis should be 4-bit gray%s", gray4bit, reset), }, { - name: "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", - cs: NewColorScheme(true, true, false, false, false, NoTheme), + name: "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + }, input: "this should be 8-bit gray", expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset), }, { - name: "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", - cs: NewColorScheme(true, true, true, false, false, NoTheme), + name: "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + }, input: "this should be 8-bit gray", expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset), }, { - name: "when 4-bit color is enabled and theme is dark, 4-bit light color is used", - cs: NewColorScheme(true, true, true, true, false, DarkTheme), + name: "when 4-bit color is enabled and theme is dark, 4-bit light color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: DarkTheme, + }, input: "this should be 4-bit dim black", expected: fmt.Sprintf("%sthis should be 4-bit dim black%s", dimBlack4bit, reset), }, { - name: "when 4-bit color is enabled and theme is light, 4-bit dark color is used", - cs: NewColorScheme(true, true, true, true, false, LightTheme), + name: "when 4-bit color is enabled and theme is light, 4-bit dark color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: LightTheme, + }, input: "this should be 4-bit bright black", expected: fmt.Sprintf("%sthis should be 4-bit bright black%s", brightBlack4bit, reset), }, { - name: "when 4-bit color is enabled but no theme, 4-bit default color is used", - cs: NewColorScheme(true, true, true, true, false, NoTheme), + name: "when 4-bit color is enabled but no theme, 4-bit default color is used", + cs: &ColorScheme{ + Enabled: true, + EightBitColor: true, + TrueColor: true, + Accessible: true, + Theme: NoTheme, + }, input: "this should have no explicit color", expected: "this should have no explicit color", }, diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 28e81abba..f5e3c2aee 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -376,7 +376,14 @@ func (s *IOStreams) TerminalWidth() int { } func (s *IOStreams) ColorScheme() *ColorScheme { - return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.AccessibleColorsEnabled(), s.ColorLabels(), s.TerminalTheme()) + return &ColorScheme{ + Enabled: s.ColorEnabled(), + EightBitColor: s.ColorSupport256(), + TrueColor: s.HasTrueColor(), + Accessible: s.AccessibleColorsEnabled(), + ColorLabels: s.ColorLabels(), + Theme: s.TerminalTheme(), + } } func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) {