Add gui.darkTheme and gui.lightTheme

A color that reads well on a dark background can be hard to read on a
light one, and the other way round. If you switch your terminal between
dark and light, there is often no single set of theme colors that works
for both (#4366).

Add two overrides of gui.theme, one for each kind of background.
gui.colorScheme, or else what the terminal tells us, decides which one
applies. A field that is set in the override replaces the one in
gui.theme. Author colors and branch color patterns are merged by entry
instead, so that an override doesn't have to repeat the entries it
doesn't change.

When the terminal's background changes, re-apply the theme and
re-render all views, not only the ones that show author colors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-09-27 08:16:06 +02:00
co-authored by Claude Opus 5.5
parent 6831c47115
commit 0ca4fdefba
6 changed files with 580 additions and 173 deletions
+33 -2
View File
@@ -163,8 +163,9 @@ 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.
# Whether the terminal has a dark or a light background. This decides whether
# 'darkTheme' or 'lightTheme' applies, and 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.
@@ -230,6 +231,16 @@ gui:
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
branchColorPatterns: {}
# Colors and styles that override those in 'theme' when the terminal has a dark
# background. It has the same fields as 'theme'.
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds
darkTheme: {}
# Colors and styles that override those in 'theme' when the terminal has a light
# background. It has the same fields as 'theme'.
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds
lightTheme: {}
# Config relating to the commit length indicator
commitLength:
# If true, show an indicator of commit message length
@@ -1030,6 +1041,26 @@ gui:
- reverse
```
## Themes for dark and light backgrounds
The colors in `gui.theme` apply whether your terminal has a dark or a light background. If you want different colors for the two, set them in `gui.darkTheme` or `gui.lightTheme`. These have the same fields as `gui.theme`, and a field that you set in them overrides the one in `gui.theme`:
```yaml
gui:
theme:
activeBorderColor:
- green
- bold
lightTheme:
activeBorderColor:
- blue
- bold
```
For `authorColors` and `branchColorPatterns`, each entry overrides the one with the same key in `gui.theme`, and the other entries of `gui.theme` still apply. Branch color patterns of `gui.darkTheme` or `gui.lightTheme` come before those of `gui.theme`.
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.
## Custom Author Color
Lazygit will assign a random color for every commit author in the commits pane by default.
+52
View File
@@ -0,0 +1,52 @@
package config
import (
"fmt"
"maps"
"reflect"
"slices"
"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 {
override := lo.Ternary(lightBackground, c.LightTheme, c.DarkTheme)
return mergeThemes(override, c.Theme)
}
// mergeThemes takes each field from the first of the themes that sets it. For
// maps and color patterns it merges the entries instead, and an entry of an
// earlier theme wins over one with the same key in a later theme.
func mergeThemes(themes ...ThemeConfig) ThemeConfig {
var result ThemeConfig
resultValue := reflect.ValueOf(&result).Elem()
for i := range resultValue.NumField() {
values := lo.Map(themes, func(theme ThemeConfig, _ int) any {
return reflect.ValueOf(theme).Field(i).Interface()
})
resultValue.Field(i).Set(reflect.ValueOf(mergeThemeField(values)))
}
return result
}
func mergeThemeField(values []any) any {
switch values[0].(type) {
case []string:
return lo.FindOrElse(lo.Map(values, func(v any, _ int) []string { return v.([]string) }), nil,
func(v []string) bool { return len(v) > 0 })
case map[string]string:
merged := map[string]string{}
for _, v := range slices.Backward(values) {
maps.Copy(merged, v.(map[string]string))
}
return merged
case ColorPatterns:
return lo.Reduce(values, func(merged ColorPatterns, v any, _ int) ColorPatterns {
return merged.over(v.(ColorPatterns))
}, nil)
default:
panic(fmt.Sprintf("don't know how to merge a theme field of type %T", values[0]))
}
}
+74
View File
@@ -0,0 +1,74 @@
package config
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestThemeForBackground(t *testing.T) {
gui := GuiConfig{
Theme: ThemeConfig{
ActiveBorderColor: []string{"green"},
InactiveBorderColor: []string{"default"},
AuthorColors: map[string]string{"Alice": "red", "Bob": "blue"},
BranchColorPatterns: ColorPatterns{
{Pattern: "^docs/", Color: "cyan"},
{Pattern: "^feature/", Color: "green"},
},
},
DarkTheme: ThemeConfig{
ActiveBorderColor: []string{"yellow"},
},
LightTheme: ThemeConfig{
InactiveBorderColor: []string{"#777777"},
AuthorColors: map[string]string{"Bob": "#000080"},
BranchColorPatterns: ColorPatterns{
{Pattern: "ISSUE", Color: "red"},
{Pattern: "^docs/", Color: "#008080"},
},
},
}
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)
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)
assert.Equal(t, ColorPatterns{
{Pattern: "ISSUE", Color: "red"},
{Pattern: "^docs/", Color: "#008080"},
{Pattern: "^feature/", Color: "green"},
}, light.BranchColorPatterns)
assert.Equal(t, map[string]string{"Alice": "red", "Bob": "blue"}, gui.Theme.AuthorColors,
"merging must leave the themes it merges alone")
}
func TestEveryThemeFieldCanBeOverridden(t *testing.T) {
var override ThemeConfig
overrideValue := reflect.ValueOf(&override).Elem()
for i := range overrideValue.NumField() {
field := overrideValue.Field(i)
switch field.Interface().(type) {
case []string:
field.Set(reflect.ValueOf([]string{"#123456"}))
case map[string]string:
field.Set(reflect.ValueOf(map[string]string{"key": "#123456"}))
case ColorPatterns:
field.Set(reflect.ValueOf(ColorPatterns{{Pattern: "key", Color: "#123456"}}))
default:
t.Fatalf("no test value for theme field %s", overrideValue.Type().Field(i).Name)
}
}
gui := GetDefaultConfig().Gui
gui.DarkTheme = override
assert.Equal(t, override, gui.ThemeForBackground(false))
}
+7 -1
View File
@@ -134,13 +134,19 @@ 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.
// Whether the terminal has a dark or a light background. This decides whether 'darkTheme' or 'lightTheme' applies, and 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"`
// Colors and styles that override those in 'theme' when the terminal has a dark background. It has the same fields as 'theme'.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds
DarkTheme ThemeConfig `yaml:"darkTheme"`
// Colors and styles that override those in 'theme' when the terminal has a light background. It has the same fields as 'theme'.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds
LightTheme ThemeConfig `yaml:"lightTheme"`
// Config relating to the commit length indicator
CommitLength CommitLengthConfig `yaml:"commitLength"`
// If true, show the '5 of 20' footer at the bottom of list views
+12 -8
View File
@@ -937,9 +937,12 @@ 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()
gui.applyTheme()
gui.configureViewProperties()
for _, context := range gui.c.Context().AllList() {
context.HandleRender()
}
gui.helpers.Refresh.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STATUS}})
gui.helpers.Diff.RenderToMainAgain()
return nil
})
@@ -1246,12 +1249,13 @@ func (gui *Gui) showBreakingChangesMessage() {
}
}
// applyTheme sets the colors of the app from the theme in the user config
// 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() {
userConfig := gui.UserConfig()
theme.UpdateTheme(userConfig.Gui.Theme)
authors.SetCustomAuthors(userConfig.Gui.Theme.AuthorColors)
presentation.SetCustomBranches(userConfig.Gui.Theme.BranchColorPatterns)
themeConfig := gui.UserConfig().Gui.ThemeForBackground(gui.terminalHasLightBackground())
theme.UpdateTheme(themeConfig)
authors.SetCustomAuthors(themeConfig.AuthorColors)
presentation.SetCustomBranches(themeConfig.BranchColorPatterns)
gui.g.FgColor = theme.InactiveBorderColor
gui.g.SelFgColor = theme.ActiveBorderColor
+402 -162
View File
@@ -712,13 +712,413 @@
"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.",
"description": "Whether the terminal has a dark or a light background. This decides whether 'darkTheme' or 'lightTheme' applies, and 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",
"properties": {
"activeBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of focused window",
"default": [
"green",
"bold"
]
},
"inactiveBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of non-focused windows",
"default": [
"default"
]
},
"searchingActiveBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of focused window when searching in that window",
"default": [
"cyan",
"bold"
]
},
"optionsTextColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Color of keybindings help text in the bottom line",
"default": [
"blue"
]
},
"selectedLineBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected line.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line",
"default": [
"blue"
]
},
"inactiveViewSelectedLineBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected line when view doesn't have focus.",
"default": [
"bold"
]
},
"cherryPickedCommitFgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Foreground color of copied commit",
"default": [
"blue"
]
},
"cherryPickedCommitBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of copied commit",
"default": [
"cyan"
]
},
"markedBaseCommitFgColor": {
"items": {
"type": "string"
},
"type": "array",
"description": "Foreground color of marked base commit (for rebase)",
"default": [
"blue"
]
},
"markedBaseCommitBgColor": {
"items": {
"type": "string"
},
"type": "array",
"description": "Background color of marked base commit (for rebase)",
"default": [
"yellow"
]
},
"unstagedChangesColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Color for file with unstaged changes",
"default": [
"red"
]
},
"defaultFgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Default text color",
"default": [
"default"
]
},
"authorColors": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color"
},
"branchColorPatterns": {
"$ref": "#/$defs/ColorPatterns",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color"
}
},
"additionalProperties": false,
"type": "object",
"description": "Config relating to colors and styles.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#color-attributes"
},
"darkTheme": {
"properties": {
"activeBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of focused window"
},
"inactiveBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of non-focused windows"
},
"searchingActiveBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of focused window when searching in that window"
},
"optionsTextColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Color of keybindings help text in the bottom line"
},
"selectedLineBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected line.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line"
},
"inactiveViewSelectedLineBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected line when view doesn't have focus."
},
"cherryPickedCommitFgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Foreground color of copied commit"
},
"cherryPickedCommitBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of copied commit"
},
"markedBaseCommitFgColor": {
"items": {
"type": "string"
},
"type": "array",
"description": "Foreground color of marked base commit (for rebase)"
},
"markedBaseCommitBgColor": {
"items": {
"type": "string"
},
"type": "array",
"description": "Background color of marked base commit (for rebase)"
},
"unstagedChangesColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Color for file with unstaged changes"
},
"defaultFgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Default text color"
},
"authorColors": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color"
},
"branchColorPatterns": {
"$ref": "#/$defs/ColorPatterns",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color"
}
},
"additionalProperties": false,
"type": "object",
"description": "Colors and styles that override those in 'theme' when the terminal has a dark background. It has the same fields as 'theme'.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds"
},
"lightTheme": {
"properties": {
"activeBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of focused window"
},
"inactiveBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of non-focused windows"
},
"searchingActiveBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of focused window when searching in that window"
},
"optionsTextColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Color of keybindings help text in the bottom line"
},
"selectedLineBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected line.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line"
},
"inactiveViewSelectedLineBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected line when view doesn't have focus."
},
"cherryPickedCommitFgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Foreground color of copied commit"
},
"cherryPickedCommitBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of copied commit"
},
"markedBaseCommitFgColor": {
"items": {
"type": "string"
},
"type": "array",
"description": "Foreground color of marked base commit (for rebase)"
},
"markedBaseCommitBgColor": {
"items": {
"type": "string"
},
"type": "array",
"description": "Background color of marked base commit (for rebase)"
},
"unstagedChangesColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Color for file with unstaged changes"
},
"defaultFgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Default text color"
},
"authorColors": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color"
},
"branchColorPatterns": {
"$ref": "#/$defs/ColorPatterns",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color"
}
},
"additionalProperties": false,
"type": "object",
"description": "Colors and styles that override those in 'theme' when the terminal has a light background. It has the same fields as 'theme'.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds"
},
"commitLength": {
"$ref": "#/$defs/CommitLengthConfig",
"description": "Config relating to the commit length indicator"
@@ -3653,166 +4053,6 @@
"type": "object",
"description": "Config relating to the spinner."
},
"ThemeConfig": {
"properties": {
"activeBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of focused window",
"default": [
"green",
"bold"
]
},
"inactiveBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of non-focused windows",
"default": [
"default"
]
},
"searchingActiveBorderColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Border color of focused window when searching in that window",
"default": [
"cyan",
"bold"
]
},
"optionsTextColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Color of keybindings help text in the bottom line",
"default": [
"blue"
]
},
"selectedLineBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected line.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line",
"default": [
"blue"
]
},
"inactiveViewSelectedLineBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected line when view doesn't have focus.",
"default": [
"bold"
]
},
"cherryPickedCommitFgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Foreground color of copied commit",
"default": [
"blue"
]
},
"cherryPickedCommitBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of copied commit",
"default": [
"cyan"
]
},
"markedBaseCommitFgColor": {
"items": {
"type": "string"
},
"type": "array",
"description": "Foreground color of marked base commit (for rebase)",
"default": [
"blue"
]
},
"markedBaseCommitBgColor": {
"items": {
"type": "string"
},
"type": "array",
"description": "Background color of marked base commit (for rebase)",
"default": [
"yellow"
]
},
"unstagedChangesColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Color for file with unstaged changes",
"default": [
"red"
]
},
"defaultFgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Default text color",
"default": [
"default"
]
},
"authorColors": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color"
},
"branchColorPatterns": {
"$ref": "#/$defs/ColorPatterns",
"description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color"
}
},
"additionalProperties": false,
"type": "object",
"description": "Config relating to colors and styles.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#color-attributes"
},
"UpdateConfig": {
"properties": {
"method": {