Pick the colors of authors for the terminal's background

The colors that lazygit derives from the names of authors are picked
for a dark background. On a light one, they are too pale to read;
against white, every author has a contrast ratio between 2.2:1 and
3.5:1.

Now that lazygit knows whether the terminal is dark or light, pick them
from a darker range of lightness when it is light. Against white, the
contrast ratio is now between 5.2:1 and 9.2:1, and against the
background of Solarized Light between 4.8:1 and 8.5:1. On a dark
background, nothing changes. If the terminal switches between dark and
light while lazygit is running, draw the commits again in the new
colors.

Add gui.colorScheme for terminals that don't tell us, and for anyone
who wants to override what they tell. It is 'auto' by default; 'dark'
and 'light' ignore what the terminal says.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-09-26 16:58:33 +02:00
co-authored by Claude Opus 5.5
parent 7282c21aa5
commit f51c4aaefc
7 changed files with 140 additions and 3 deletions
+9
View File
@@ -169,6 +169,13 @@ gui:
# Uses Go's time format syntax: https://pkg.go.dev/time#Time.Format
shortTimeFormat: 3:04PM
# Whether the terminal has a dark or a light background. The colors of authors
# are picked to stand out against it.
# One of: 'auto' (default) | 'dark' | 'light'
# With 'auto', lazygit asks the terminal, and assumes a dark background if the
# terminal doesn't tell.
colorScheme: auto
# Config relating to colors and styles.
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#color-attributes
theme:
@@ -1027,6 +1034,8 @@ gui:
Lazygit will assign a random color for every commit author in the commits pane by default.
These colors are picked to be readable against the background of your terminal, and lazygit asks the terminal whether its background is dark or light. If your terminal doesn't tell, lazygit assumes a dark background; set `gui.colorScheme` to `light` if yours is light.
You can customize the color in case you're not happy with the randomly assigned one:
```yaml
+5
View File
@@ -141,6 +141,10 @@ type GuiConfig struct {
// Format used when displaying time if the time is less than 24 hours ago.
// Uses Go's time format syntax: https://pkg.go.dev/time#Time.Format
ShortTimeFormat string `yaml:"shortTimeFormat"`
// Whether the terminal has a dark or a light background. The colors of authors are picked to stand out against it.
// One of: 'auto' (default) | 'dark' | 'light'
// With 'auto', lazygit asks the terminal, and assumes a dark background if the terminal doesn't tell.
ColorScheme string `yaml:"colorScheme" jsonschema:"enum=auto,enum=dark,enum=light"`
// Config relating to colors and styles.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#color-attributes
Theme ThemeConfig `yaml:"theme"`
@@ -881,6 +885,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
Language: "auto",
TimeFormat: "02 Jan 06",
ShortTimeFormat: time.Kitchen,
ColorScheme: "auto",
Theme: ThemeConfig{
ActiveBorderColor: []string{"green", "bold"},
SearchingActiveBorderColor: []string{"cyan", "bold"},
+4
View File
@@ -18,6 +18,10 @@ func (config *UserConfig) Validate() error {
[]string{"dashboard", "allBranchesLog"}); err != nil {
return err
}
if err := validateEnum("gui.colorScheme", config.Gui.ColorScheme,
[]string{"auto", "dark", "light"}); err != nil {
return err
}
if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch,
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
return err
+24
View File
@@ -945,6 +945,9 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
gui.c.Log.Infof("Terminal color scheme: %s", g.DetectedColorScheme())
g.SetColorSchemeChangeHandler(func(colorScheme gocui.DetectedColorScheme) error {
gui.c.Log.Infof("Terminal color scheme changed: %s", colorScheme)
gui.applyTerminalBackground()
gui.c.Contexts().LocalCommits.HandleRender()
gui.c.Contexts().SubCommits.HandleRender()
return nil
})
@@ -1259,6 +1262,27 @@ func (gui *Gui) setColorScheme() {
gui.g.SelFgColor = theme.ActiveBorderColor
gui.g.FrameColor = theme.InactiveBorderColor
gui.g.SelFrameColor = theme.ActiveBorderColor
gui.applyTerminalBackground()
}
// applyTerminalBackground tells the colors that depend on the terminal's
// background whether it is light.
func (gui *Gui) applyTerminalBackground() {
authors.SetLightBackground(gui.terminalHasLightBackground())
}
// terminalHasLightBackground goes by gui.colorScheme, or by what the terminal
// tells us if that is 'auto'.
func (gui *Gui) terminalHasLightBackground() bool {
switch gui.UserConfig().Gui.ColorScheme {
case "dark":
return false
case "light":
return true
default:
return gui.g.DetectedColorScheme().ColorScheme == gocui.ColorSchemeLight
}
}
func (gui *Gui) onUIThread(f func() error) {
+25 -3
View File
@@ -27,6 +27,10 @@ var (
// The styles derived from the names of the other authors
authorStyleCache = make(map[string]*style.TextStyle)
// Whether the terminal has a light background, for the derived styles to
// stand out against
lightBackground bool
colorsVersion int
)
@@ -114,13 +118,19 @@ func colorAtPosition(hue, saturation, lightness float64) colorful.Color {
// HSL spreads them instead. At one and the same lightness, it gives a
// glaring yellow and a blue that all but disappears.
//
// The lightness range keeps every author above a contrast ratio of 4.5:1
// against common dark backgrounds, such as #1e1e1e.
// There is one lightness range for a dark background and one for a light
// background. Each keeps every author above a contrast ratio of 4.5:1
// against common backgrounds of its kind, such as #1e1e1e and #fdf6e3.
//
// Saturation in HSLuv is a fraction of the most colorful a hue can get at
// that lightness, and pale colors are hard to tell apart, so keep it near
// the top of its range.
return colorful.HSLuv(hue*360.0, 0.8+0.2*saturation, 0.57+0.15*lightness)
minLightness := 0.57
if lightBackground {
minLightness = 0.31
}
return colorful.HSLuv(hue*360.0, 0.8+0.2*saturation, minLightness+0.15*lightness)
}
// ColorPosition says where an author's color lies within the range of hues,
@@ -166,6 +176,18 @@ func SetCustomAuthors(customAuthorColors map[string]string) {
colorsChanged()
}
// SetLightBackground says whether the terminal has a light background, for the
// colors of authors to stand out against.
func SetLightBackground(light bool) {
if light == lightBackground {
return
}
lightBackground = light
authorStyleCache = make(map[string]*style.TextStyle)
colorsChanged()
}
// colorsChanged drops what was rendered with the previous colors of authors.
func colorsChanged() {
authorInitialCache = make(map[string]string)
@@ -6,6 +6,7 @@ import (
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/lucasb-eyer/go-colorful"
"github.com/stretchr/testify/assert"
"github.com/xo/terminfo"
)
@@ -58,3 +59,65 @@ func TestAuthorColorsFollowTheConfig(t *testing.T) {
assert.Equal(t, style.FgBlue.Sprint("JD"), ShortAuthor("Jane Doe"))
assert.Equal(t, style.FgBlue.Sprint("Jane Doe"), LongAuthor("Jane Doe", 8))
}
func TestSetLightBackground(t *testing.T) {
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)
t.Cleanup(func() {
SetLightBackground(false)
SetCustomAuthors(nil)
})
SetCustomAuthors(map[string]string{"Jane Doe": "red"})
onDarkBackground := ShortAuthor("John Smith")
SetLightBackground(true)
assert.NotEqual(t, onDarkBackground, ShortAuthor("John Smith"))
assert.Equal(t, style.FgRed.Sprint("JD"), ShortAuthor("Jane Doe"))
}
func TestAuthorColorsStandOutAgainstTheBackground(t *testing.T) {
t.Cleanup(func() { SetLightBackground(false) })
scenarios := []struct {
name string
lightBackground bool
backgrounds []string
}{
{name: "dark", lightBackground: false, backgrounds: []string{"#000000", "#1e1e1e"}},
{name: "light", lightBackground: true, backgrounds: []string{"#ffffff", "#fdf6e3"}},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
SetLightBackground(s.lightBackground)
for _, backgroundHex := range s.backgrounds {
background, err := colorful.Hex(backgroundHex)
assert.NoError(t, err)
// The edges of the range, which is where the contrast is lowest
for hue := range 100 {
for _, saturation := range []float64{0, 0.99} {
for _, lightness := range []float64{0, 0.99} {
c := colorAtPosition(float64(hue)/100, saturation, lightness)
assert.GreaterOrEqual(t, contrastRatio(c, background), 4.5,
"%s on %s", c.Hex(), backgroundHex)
}
}
}
}
})
}
}
// contrastRatio is as defined by the Web Content Accessibility Guidelines
func contrastRatio(a colorful.Color, b colorful.Color) float64 {
luminance := func(c colorful.Color) float64 {
r, g, b := c.LinearRgb()
return 0.2126*r + 0.7152*g + 0.0722*b
}
lighter := max(luminance(a), luminance(b))
darker := min(luminance(a), luminance(b))
return (lighter + 0.05) / (darker + 0.05)
}
+10
View File
@@ -719,6 +719,16 @@
"description": "Format used when displaying time if the time is less than 24 hours ago.\nUses Go's time format syntax: https://pkg.go.dev/time#Time.Format",
"default": "3:04PM"
},
"colorScheme": {
"type": "string",
"enum": [
"auto",
"dark",
"light"
],
"description": "Whether the terminal has a dark or a light background. The colors of authors are picked to stand out against it.\nOne of: 'auto' (default) | 'dark' | 'light'\nWith 'auto', lazygit asks the terminal, and assumes a dark background if the terminal doesn't tell.",
"default": "auto"
},
"theme": {
"$ref": "#/$defs/ThemeConfig",
"description": "Config relating to colors and styles.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#color-attributes"