From ca950181e865b4777118e264a6f8dc8d69ee6c26 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 26 Sep 2026 14:47:54 +0200 Subject: [PATCH] Pick the built-in theme defaults for the terminal's background Some theme colors can't have one default that suits both dark and light backgrounds. A gray background for the selected line of an inactive view has to be lighter than the terminal's background if that is dark, and darker if it is light. Give such fields a default for each kind of background. Derive it from the terminal's background color if the terminal tells us, so that it keeps the same distance from the background however dark or light that is, and takes on its tint. Otherwise, assume a black or white background. These defaults can't be the defaults of gui.theme. We would then have to tell whether a value there came from the user or from the built-in defaults, because only the user's value should win over a background default. Keep them apart from the user config instead, and leave these fields empty in the defaults of gui.theme, so that a value there always comes from the user. Tests ensure that no field has both kinds of defaults, and that both kinds of background set the same fields. Start with inactiveViewSelectedLineBgColor: the background mixed with 30% white if it is dark (#4d4d4d on black), or with 15% black if it is light (#d9d9d9 on white). A gray background shows where the selection is in a view without the focus more clearly than the bold text we used so far. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs-master/Config.md | 4 +-- pkg/config/theme_config.go | 50 ++++++++++++++++++++++++++++-- pkg/config/theme_config_test.go | 55 +++++++++++++++++++++++++++++++-- pkg/config/user_config.go | 24 +++++++------- pkg/gui/gui.go | 13 +++++++- schema-master/config.json | 9 ++---- 6 files changed, 128 insertions(+), 27 deletions(-) diff --git a/docs-master/Config.md b/docs-master/Config.md index 1452694b7..3fb2ba655 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -198,8 +198,8 @@ gui: - blue # Background color of selected line when view doesn't have focus. - inactiveViewSelectedLineBgColor: - - bold + # Default: a suitable RGB grey computed from the terminal's background color. + inactiveViewSelectedLineBgColor: [] # Foreground color of copied commit cherryPickedCommitFgColor: diff --git a/pkg/config/theme_config.go b/pkg/config/theme_config.go index 81554bf8e..04fdf3726 100644 --- a/pkg/config/theme_config.go +++ b/pkg/config/theme_config.go @@ -3,17 +3,61 @@ package config import ( "fmt" "maps" + "math" "reflect" "slices" + "strconv" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) // ThemeForBackground returns gui.theme with the overrides for a dark or a light -// background applied. -func (c *GuiConfig) ThemeForBackground(lightBackground bool) ThemeConfig { +// background applied, and with the defaults for that background in the fields +// that neither of them sets. backgroundColor is the terminal's background color +// as #rrggbb, or empty if we don't know it. +func (c *GuiConfig) ThemeForBackground(lightBackground bool, backgroundColor string) ThemeConfig { override := lo.Ternary(lightBackground, c.LightTheme, c.DarkTheme) - return mergeThemes(override, c.Theme) + return mergeThemes(override, c.Theme, themeDefaults(lightBackground, backgroundColor)) +} + +// themeDefaults returns the defaults of the theme fields whose default depends +// on the terminal's background. GetDefaultConfig leaves these fields empty in +// gui.theme, so that a value there comes from the user, and wins over these. +// +// They are derived from the background color, so that they keep the same +// distance from it however dark or light it is. If we don't know the +// background color, we assume black or white. +func themeDefaults(lightBackground bool, backgroundColor string) ThemeConfig { + if len(backgroundColor) != 7 || !utils.IsValidHexValue(backgroundColor) { + backgroundColor = lo.Ternary(lightBackground, "#ffffff", "#000000") + } + + if lightBackground { + return ThemeConfig{ + InactiveViewSelectedLineBgColor: []string{mixHexColors(backgroundColor, "#000000", 0.15)}, + } + } + + return ThemeConfig{ + InactiveViewSelectedLineBgColor: []string{mixHexColors(backgroundColor, "#ffffff", 0.3)}, + } +} + +// mixHexColors mixes two colors given as #rrggbb. amount is how much of b to +// take: 0 gives a, 1 gives b. +func mixHexColors(a, b string, amount float64) string { + channel := func(color string, i int) float64 { + value, _ := strconv.ParseUint(color[1+2*i:3+2*i], 16, 8) + return float64(value) + } + + result := "#" + for i := range 3 { + mixed := channel(a, i) + (channel(b, i)-channel(a, i))*amount + result += fmt.Sprintf("%02x", int(math.Round(mixed))) + } + return result } // mergeThemes takes each field from the first of the themes that sets it. For diff --git a/pkg/config/theme_config_test.go b/pkg/config/theme_config_test.go index 1971a895b..a4bd39028 100644 --- a/pkg/config/theme_config_test.go +++ b/pkg/config/theme_config_test.go @@ -31,13 +31,13 @@ func TestThemeForBackground(t *testing.T) { }, } - dark := gui.ThemeForBackground(false) + dark := gui.ThemeForBackground(false, "") assert.Equal(t, []string{"yellow"}, dark.ActiveBorderColor) assert.Equal(t, []string{"default"}, dark.InactiveBorderColor) assert.Equal(t, map[string]string{"Alice": "red", "Bob": "blue"}, dark.AuthorColors) assert.Equal(t, gui.Theme.BranchColorPatterns, dark.BranchColorPatterns) - light := gui.ThemeForBackground(true) + light := gui.ThemeForBackground(true, "") assert.Equal(t, []string{"green"}, light.ActiveBorderColor) assert.Equal(t, []string{"#777777"}, light.InactiveBorderColor) assert.Equal(t, map[string]string{"Alice": "red", "Bob": "#000080"}, light.AuthorColors) @@ -70,5 +70,54 @@ func TestEveryThemeFieldCanBeOverridden(t *testing.T) { gui := GetDefaultConfig().Gui gui.DarkTheme = override - assert.Equal(t, override, gui.ThemeForBackground(false)) + assert.Equal(t, override, gui.ThemeForBackground(false, "")) +} + +func TestThemeForBackgroundFallsBackToTheDefaultsForTheBackground(t *testing.T) { + gui := GetDefaultConfig().Gui + assert.Equal(t, []string{"#4d4d4d"}, gui.ThemeForBackground(false, "").InactiveViewSelectedLineBgColor) + assert.Equal(t, []string{"#d9d9d9"}, gui.ThemeForBackground(true, "").InactiveViewSelectedLineBgColor) + + gui.Theme.InactiveViewSelectedLineBgColor = []string{"bold"} + assert.Equal(t, []string{"bold"}, gui.ThemeForBackground(false, "").InactiveViewSelectedLineBgColor) + assert.Equal(t, []string{"bold"}, gui.ThemeForBackground(true, "").InactiveViewSelectedLineBgColor) + + gui.LightTheme.InactiveViewSelectedLineBgColor = []string{"white"} + assert.Equal(t, []string{"bold"}, gui.ThemeForBackground(false, "").InactiveViewSelectedLineBgColor) + assert.Equal(t, []string{"white"}, gui.ThemeForBackground(true, "").InactiveViewSelectedLineBgColor) +} + +func TestBackgroundDefaultsAreDerivedFromTheBackgroundColor(t *testing.T) { + gui := GetDefaultConfig().Gui + assert.Equal(t, []string{"#626262"}, gui.ThemeForBackground(false, "#1e1e1e").InactiveViewSelectedLineBgColor) + assert.Equal(t, []string{"#d7d1c1"}, gui.ThemeForBackground(true, "#fdf6e3").InactiveViewSelectedLineBgColor) +} + +// If gui.theme had a default for a field that also has a default for the +// terminal's background, the former would always win. +func TestFieldsWithBackgroundDefaultsHaveNoDefaultInGuiTheme(t *testing.T) { + genericDefaults := setThemeFields(GetDefaultConfig().Gui.Theme) + for _, field := range setThemeFields(themeDefaults(false, "")) { + assert.NotContains(t, genericDefaults, field) + } + for _, field := range setThemeFields(themeDefaults(true, "")) { + assert.NotContains(t, genericDefaults, field) + } +} + +// If only one of the backgrounds had a default for a field, the field would +// have no value at all with the other. +func TestDarkAndLightDefaultsSetTheSameFields(t *testing.T) { + assert.Equal(t, setThemeFields(themeDefaults(false, "")), setThemeFields(themeDefaults(true, ""))) +} + +func setThemeFields(theme ThemeConfig) []string { + var fields []string + themeValue := reflect.ValueOf(theme) + for i := range themeValue.NumField() { + if themeValue.Field(i).Len() > 0 { + fields = append(fields, themeValue.Type().Field(i).Name) + } + } + return fields } diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 01621f354..98a4a16c9 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -244,6 +244,7 @@ type ThemeConfig struct { // See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line SelectedLineBgColor []string `yaml:"selectedLineBgColor" jsonschema:"minItems=1,uniqueItems=true"` // Background color of selected line when view doesn't have focus. + // Default: a suitable RGB grey computed from the terminal's background color. InactiveViewSelectedLineBgColor []string `yaml:"inactiveViewSelectedLineBgColor" jsonschema:"minItems=1,uniqueItems=true"` // Foreground color of copied commit CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor" jsonschema:"minItems=1,uniqueItems=true"` @@ -890,18 +891,17 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { ShortTimeFormat: time.Kitchen, ColorScheme: "auto", Theme: ThemeConfig{ - ActiveBorderColor: []string{"green", "bold"}, - SearchingActiveBorderColor: []string{"cyan", "bold"}, - InactiveBorderColor: []string{"default"}, - OptionsTextColor: []string{"blue"}, - SelectedLineBgColor: []string{"blue"}, - InactiveViewSelectedLineBgColor: []string{"bold"}, - CherryPickedCommitBgColor: []string{"cyan"}, - CherryPickedCommitFgColor: []string{"blue"}, - MarkedBaseCommitBgColor: []string{"yellow"}, - MarkedBaseCommitFgColor: []string{"blue"}, - UnstagedChangesColor: []string{"red"}, - DefaultFgColor: []string{"default"}, + ActiveBorderColor: []string{"green", "bold"}, + SearchingActiveBorderColor: []string{"cyan", "bold"}, + InactiveBorderColor: []string{"default"}, + OptionsTextColor: []string{"blue"}, + SelectedLineBgColor: []string{"blue"}, + CherryPickedCommitBgColor: []string{"cyan"}, + CherryPickedCommitFgColor: []string{"blue"}, + MarkedBaseCommitBgColor: []string{"yellow"}, + MarkedBaseCommitFgColor: []string{"blue"}, + UnstagedChangesColor: []string{"red"}, + DefaultFgColor: []string{"default"}, }, CommitLength: CommitLengthConfig{Show: true}, SkipNoStagedFilesWarning: false, diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index ebfc6d5f2..8d66e99b1 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -1252,7 +1252,7 @@ func (gui *Gui) showBreakingChangesMessage() { // applyTheme sets the colors of the app from the theme in the user config, // with the overrides for the terminal's background applied func (gui *Gui) applyTheme() { - themeConfig := gui.UserConfig().Gui.ThemeForBackground(gui.terminalHasLightBackground()) + themeConfig := gui.UserConfig().Gui.ThemeForBackground(gui.terminalHasLightBackground(), gui.terminalBackgroundColor()) theme.UpdateTheme(themeConfig) authors.SetCustomAuthors(themeConfig.AuthorColors) presentation.SetCustomBranches(themeConfig.BranchColorPatterns) @@ -1284,6 +1284,17 @@ func (gui *Gui) terminalHasLightBackground() bool { } } +// terminalBackgroundColor returns the background color that the terminal told +// us, as #rrggbb. It returns "" if the terminal didn't tell us, or if +// gui.colorScheme disagrees with it about whether the background is light. +func (gui *Gui) terminalBackgroundColor() string { + detected := gui.g.DetectedColorScheme() + if (detected.ColorScheme == gocui.ColorSchemeLight) != gui.terminalHasLightBackground() { + return "" + } + return detected.Background +} + func (gui *Gui) onUIThread(f func() error) { gui.g.Update(func(*gocui.Gui) error { return f() diff --git a/schema-master/config.json b/schema-master/config.json index a00e6cc5b..00b398c17 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -786,10 +786,7 @@ "type": "array", "minItems": 1, "uniqueItems": true, - "description": "Background color of selected line when view doesn't have focus.", - "default": [ - "bold" - ] + "description": "Background color of selected line when view doesn't have focus.\nDefault: a suitable RGB grey computed from the terminal's background color." }, "cherryPickedCommitFgColor": { "items": { @@ -929,7 +926,7 @@ "type": "array", "minItems": 1, "uniqueItems": true, - "description": "Background color of selected line when view doesn't have focus." + "description": "Background color of selected line when view doesn't have focus.\nDefault: a suitable RGB grey computed from the terminal's background color." }, "cherryPickedCommitFgColor": { "items": { @@ -1051,7 +1048,7 @@ "type": "array", "minItems": 1, "uniqueItems": true, - "description": "Background color of selected line when view doesn't have focus." + "description": "Background color of selected line when view doesn't have focus.\nDefault: a suitable RGB grey computed from the terminal's background color." }, "cherryPickedCommitFgColor": { "items": {