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": {