Refactor ColorScheme initializer

This commit completely removes the iostreams.NewColorScheme() initializer function in favor of exporting the type fields for greater clarity in its use.

The result being code specifying only the fields that matter to test cases.
This commit is contained in:
Andy Feller 2025-04-04 11:57:37 -04:00
parent addbc6ac5c
commit e067eacd81
5 changed files with 189 additions and 94 deletions

View file

@ -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
}

View file

@ -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)

View file

@ -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:

View file

@ -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",
},

View file

@ -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) {