Files
Stefan HallerandClaude Opus 5.5 f51c4aaefc 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>
2026-09-26 16:58:33 +02:00

204 lines
5.6 KiB
Go

package authors
import (
"crypto/md5"
"strings"
"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/rivo/uniseg"
)
type authorNameCacheKey struct {
authorName string
truncateTo int
}
// if these being global variables causes trouble we can wrap them in a struct
// attached to the gui state.
var (
authorInitialCache = make(map[string]string)
authorNameCache = make(map[authorNameCacheKey]string)
// The styles from gui.authorColors
customAuthorStyles = make(map[string]*style.TextStyle)
// 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
)
const authorNameWildcard = "*"
func ShortAuthor(authorName string) string {
if value, ok := authorInitialCache[authorName]; ok {
return value
}
initials := getInitials(authorName)
if initials == "" {
return ""
}
value := AuthorStyle(authorName).Sprint(initials)
authorInitialCache[authorName] = value
return value
}
func LongAuthor(authorName string, length int) string {
cacheKey := authorNameCacheKey{authorName: authorName, truncateTo: length}
if value, ok := authorNameCache[cacheKey]; ok {
return value
}
paddedAuthorName := utils.WithPadding(authorName, length, utils.AlignLeft)
truncatedName := utils.TruncateWithEllipsis(paddedAuthorName, length)
value := AuthorStyle(authorName).Sprint(truncatedName)
authorNameCache[cacheKey] = value
return value
}
// AuthorWithLength returns a representation of the author that fits into a
// given maximum length:
// - if the length is less than 2, it returns an empty string
// - if the length is 2, it returns the initials
// - otherwise, it returns the author name truncated to the maximum length
func AuthorWithLength(authorName string, length int) string {
if length < 2 {
return ""
}
if length == 2 {
return ShortAuthor(authorName)
}
return LongAuthor(authorName, length)
}
func AuthorStyle(authorName string) *style.TextStyle {
if value, ok := customAuthorStyles[authorName]; ok {
return value
}
// use the unified style whatever the author name is
if value, ok := customAuthorStyles[authorNameWildcard]; ok {
return value
}
if value, ok := authorStyleCache[authorName]; ok {
return value
}
value := trueColorStyle(authorName)
authorStyleCache[authorName] = &value
return &value
}
func trueColorStyle(str string) style.TextStyle {
c := colorAtPosition(ColorPosition(str))
return style.New().SetFg(style.NewRGBColor(color.RGB(uint8(c.R*255), uint8(c.G*255), uint8(c.B*255))))
}
// To check the colors at the edges of the ranges below, run
// `go run ./cmd/author_colors_repo <path>` and open the repository it creates.
func colorAtPosition(hue, saturation, lightness float64) colorful.Color {
// The lightness of an HSLuv color is how bright it looks, so every author
// comes out about equally readable whichever hue their name lands on. Plain
// HSL spreads them instead. At one and the same lightness, it gives a
// glaring yellow and a blue that all but disappears.
//
// 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.
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,
// saturations and lightnesses that colorAtPosition picks from. Each is a
// fraction from 0 up to 1, derived from a hash of the author's name.
func ColorPosition(authorName string) (hue, saturation, lightness float64) {
hash := md5.Sum([]byte(authorName))
return randFloat(hash[0:4]), randFloat(hash[4:8]), randFloat(hash[8:12])
}
func randFloat(hash []byte) float64 {
return float64(randInt(hash, 100)) / 100
}
func randInt(hash []byte, max int) int {
sum := 0
for _, b := range hash {
sum = (sum + int(b)) % max
}
return sum
}
func getInitials(authorName string) string {
if authorName == "" {
return authorName
}
firstChar, _, width, _ := uniseg.FirstGraphemeClusterInString(authorName, -1)
if width > 1 {
return firstChar
}
split := strings.Split(authorName, " ")
if len(split) == 1 {
return utils.LimitStr(authorName, 2)
}
return utils.LimitStr(split[0], 1) + utils.LimitStr(split[1], 1)
}
func SetCustomAuthors(customAuthorColors map[string]string) {
customAuthorStyles = utils.SetCustomColors(customAuthorColors)
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)
authorNameCache = make(map[authorNameCacheKey]string)
colorsVersion++
}
// ColorsVersion changes whenever the colors of authors change, so that
// whatever keeps the styles of authors around can tell when they are out of
// date.
func ColorsVersion() int {
return colorsVersion
}