From f51c4aaefc99b45630486a6f35d267512edeef31 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 25 Sep 2026 13:39:24 +0200 Subject: [PATCH] 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) --- docs-master/Config.md | 9 +++ pkg/config/user_config.go | 5 ++ pkg/config/user_config_validation.go | 4 ++ pkg/gui/gui.go | 24 ++++++++ pkg/gui/presentation/authors/authors.go | 28 ++++++++- pkg/gui/presentation/authors/authors_test.go | 63 ++++++++++++++++++++ schema-master/config.json | 10 ++++ 7 files changed, 140 insertions(+), 3 deletions(-) diff --git a/docs-master/Config.md b/docs-master/Config.md index 857a4e359..118d26138 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -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 diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 9738186d9..d05808159 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -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"}, diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 3f836a21b..2e7ab4cf8 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -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 diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 25f460483..e1c921b7a 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -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) { diff --git a/pkg/gui/presentation/authors/authors.go b/pkg/gui/presentation/authors/authors.go index 765469e86..100c7603b 100644 --- a/pkg/gui/presentation/authors/authors.go +++ b/pkg/gui/presentation/authors/authors.go @@ -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) diff --git a/pkg/gui/presentation/authors/authors_test.go b/pkg/gui/presentation/authors/authors_test.go index 8958ada1a..89b8f8df0 100644 --- a/pkg/gui/presentation/authors/authors_test.go +++ b/pkg/gui/presentation/authors/authors_test.go @@ -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) +} diff --git a/schema-master/config.json b/schema-master/config.json index 45a2b9efe..9434f6016 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -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"