Ensure table headers are thematically contrasting

This commit refactors the color format around table headers to ensure the GitHub CLI uses thematically appropriate colors based on dark background, light background, or no color at all.

In order to do so, `ColorScheme` needs information from the terminal about the background appearance (dark, light, none) to determine appropriate muted color.
This commit is contained in:
Andy Feller 2025-03-21 11:51:03 -04:00
parent cf3f0ee6b8
commit 6355e54e3c
5 changed files with 51 additions and 32 deletions

View file

@ -80,7 +80,7 @@ func NewWithWriter(w io.Writer, isTTY bool, maxWidth int, cs *iostreams.ColorSch
tp.AddHeader(
upperCasedHeaders,
WithPadding(paddingFunc),
WithColor(cs.LightGrayUnderline),
WithColor(cs.TableHeader),
)
}

View file

@ -694,7 +694,7 @@ func Test_highlightMatch(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cs := iostreams.NewColorScheme(tt.color, false, false)
cs := iostreams.NewColorScheme(tt.color, false, false, iostreams.NoTheme)
matched := false
got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight)

View file

@ -9,34 +9,44 @@ import (
)
const (
NoTheme = "none"
DarkTheme = "dark"
LightTheme = "light"
highlightStyle = "black:yellow"
)
// Special cases like darkTableHeader / lightTableHeader are necessary when using color and modifiers
// (bold, underline, dim) because ansi.ColorFunc requires a foreground color and resets formats.
var (
magenta = ansi.ColorFunc("magenta")
cyan = ansi.ColorFunc("cyan")
red = ansi.ColorFunc("red")
yellow = ansi.ColorFunc("yellow")
blue = ansi.ColorFunc("blue")
green = ansi.ColorFunc("green")
gray = ansi.ColorFunc("black+h")
lightGrayUnderline = ansi.ColorFunc("white+du")
bold = ansi.ColorFunc("default+b")
cyanBold = ansi.ColorFunc("cyan+b")
greenBold = ansi.ColorFunc("green+b")
highlightStart = ansi.ColorCode(highlightStyle)
highlight = ansi.ColorFunc(highlightStyle)
magenta = ansi.ColorFunc("magenta")
cyan = ansi.ColorFunc("cyan")
red = ansi.ColorFunc("red")
yellow = ansi.ColorFunc("yellow")
blue = ansi.ColorFunc("blue")
green = ansi.ColorFunc("green")
gray = ansi.ColorFunc("black+h")
bold = ansi.ColorFunc("default+b")
cyanBold = ansi.ColorFunc("cyan+b")
greenBold = ansi.ColorFunc("green+b")
highlightStart = ansi.ColorCode(highlightStyle)
highlight = ansi.ColorFunc(highlightStyle)
darkTableHeader = ansi.ColorFunc("white+du")
lightTableHeader = ansi.ColorFunc("black+hu")
gray256 = func(t string) string {
return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t)
}
)
func NewColorScheme(enabled, is256enabled bool, trueColor bool) *ColorScheme {
// 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,
// and terminal theme detected.
func NewColorScheme(enabled, is256enabled, trueColor bool, theme string) *ColorScheme {
return &ColorScheme{
enabled: enabled,
is256enabled: is256enabled,
hasTrueColor: trueColor,
theme: theme,
}
}
@ -44,6 +54,7 @@ type ColorScheme struct {
enabled bool
is256enabled bool
hasTrueColor bool
theme string
}
func (c *ColorScheme) Enabled() bool {
@ -115,13 +126,6 @@ func (c *ColorScheme) Grayf(t string, args ...interface{}) string {
return c.Gray(fmt.Sprintf(t, args...))
}
func (c *ColorScheme) LightGrayUnderline(t string) string {
if !c.enabled {
return t
}
return lightGrayUnderline(t)
}
func (c *ColorScheme) Magenta(t string) string {
if !c.enabled {
return t
@ -254,3 +258,18 @@ func (c *ColorScheme) HexToRGB(hex string, x string) string {
b, _ := strconv.ParseInt(hex[4:6], 16, 64)
return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x)
}
func (c *ColorScheme) TableHeader(t string) string {
if !c.enabled {
return t
}
switch c.theme {
case DarkTheme:
return darkTableHeader(t)
case LightTheme:
return lightTableHeader(t)
default:
return t
}
}

View file

@ -19,28 +19,28 @@ func TestColorFromRGB(t *testing.T) {
hex: "fc0303",
text: "red",
wants: "\033[38;2;252;3;3mred\033[0m",
cs: NewColorScheme(true, true, true),
cs: NewColorScheme(true, true, true, NoTheme),
},
{
name: "no truecolor",
hex: "fc0303",
text: "red",
wants: "red",
cs: NewColorScheme(true, true, false),
cs: NewColorScheme(true, true, false, NoTheme),
},
{
name: "no color",
hex: "fc0303",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false),
cs: NewColorScheme(false, false, false, NoTheme),
},
{
name: "invalid hex",
hex: "fc0",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false),
cs: NewColorScheme(false, false, false, NoTheme),
},
}
@ -63,28 +63,28 @@ func TestHexToRGB(t *testing.T) {
hex: "fc0303",
text: "red",
wants: "\033[38;2;252;3;3mred\033[0m",
cs: NewColorScheme(true, true, true),
cs: NewColorScheme(true, true, true, NoTheme),
},
{
name: "no truecolor",
hex: "fc0303",
text: "red",
wants: "red",
cs: NewColorScheme(true, true, false),
cs: NewColorScheme(true, true, false, NoTheme),
},
{
name: "no color",
hex: "fc0303",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false),
cs: NewColorScheme(false, false, false, NoTheme),
},
{
name: "invalid hex",
hex: "fc0",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false),
cs: NewColorScheme(false, false, false, NoTheme),
},
}

View file

@ -366,7 +366,7 @@ func (s *IOStreams) TerminalWidth() int {
}
func (s *IOStreams) ColorScheme() *ColorScheme {
return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor())
return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.TerminalTheme())
}
func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) {