From d4f6e77d942302d7798991391820e7de929109a4 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 25 Sep 2026 14:27:02 +0200 Subject: [PATCH] Add a tool that creates a repository of authors at the color extremes If gui.authorColors names no color for an author, lazygit derives one from a hash of their name. Checking whether these colors are readable means looking at a repository, but the authors of a real one rarely land near the edges of the range, so the worst cases go unseen. Add cmd/author_colors_repo. It searches for names at the lowest and highest lightness and saturation, at twelve hues, and commits once as each of them. The names depend only on the hashing, so the same repository shows the colors of any lazygit build and can be kept around to compare them. Co-Authored-By: Claude Opus 5.5 (1M context) --- cmd/author_colors_repo/main.go | 142 ++++++++++++++++++++++++ pkg/gui/presentation/authors/authors.go | 2 + 2 files changed, 144 insertions(+) create mode 100644 cmd/author_colors_repo/main.go diff --git a/cmd/author_colors_repo/main.go b/cmd/author_colors_repo/main.go new file mode 100644 index 000000000..cebfaa39b --- /dev/null +++ b/cmd/author_colors_repo/main.go @@ -0,0 +1,142 @@ +// author_colors_repo creates a git repository for checking that the colors +// lazygit gives to authors are readable. +// +// If gui.authorColors names no color for an author, lazygit derives one from a +// hash of their name. The authors of an ordinary repository rarely land near the +// edges of the range that this color is picked from. Every commit in the +// repository created here is by an author at one of those edges: the lowest or +// highest lightness, combined with the lowest or highest saturation, at twelve +// hues around the color wheel. The commit subject says which edge it is. +// +// Usage: +// +// go run ./cmd/author_colors_repo +// +// Then open the repository with lazygit in each terminal theme you want to check. +package main + +import ( + "fmt" + "log" + "math" + "os" + "os/exec" + "time" + + "github.com/jesseduffield/lazygit/pkg/gui/presentation/authors" +) + +type extreme int + +const ( + lowest extreme = iota + highest +) + +func (self extreme) String() string { + if self == lowest { + return "min" + } + return "max" +} + +func (self extreme) matches(fraction float64) bool { + if self == lowest { + return fraction < 0.01 + } + return fraction >= 0.99 +} + +const ( + numHues = 12 + + // Small enough that the windows of neighbouring hues don't overlap + hueTolerance = 0.02 +) + +type commit struct { + author string + subject string +} + +func main() { + if len(os.Args) != 2 { + log.Fatalf("usage: %s ", os.Args[0]) + } + path := os.Args[1] + if _, err := os.Stat(path); err == nil { + log.Fatalf("%s already exists", path) + } + + extremes := []extreme{lowest, highest} + commits := make([]commit, 0, len(extremes)*len(extremes)*numHues) + for _, lightness := range extremes { + for _, saturation := range extremes { + for i := range numHues { + hue := float64(i) / numHues + author, actualHue := findAuthor(lightness, saturation, hue) + subject := fmt.Sprintf("lightness %s, saturation %s, hue %.0f°", lightness, saturation, actualHue*360) + commits = append(commits, commit{author: author, subject: subject}) + } + } + } + + if err := runGit("", nil, "init", "-q", path); err != nil { + log.Fatal(err) + } + + // Commit in reverse, so that the commits panel lists them in the order above + startTime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + for i := range commits { + c := commits[len(commits)-1-i] + date := fmt.Sprintf("%d +0000", startTime.Add(time.Duration(i)*time.Hour).Unix()) + env := []string{ + "GIT_AUTHOR_NAME=" + c.author, + "GIT_AUTHOR_EMAIL=author@example.com", + "GIT_AUTHOR_DATE=" + date, + "GIT_COMMITTER_NAME=" + c.author, + "GIT_COMMITTER_EMAIL=author@example.com", + "GIT_COMMITTER_DATE=" + date, + } + if err := runGit(path, env, "commit", "-q", "--allow-empty", "-m", c.subject); err != nil { + log.Fatal(err) + } + } + + fmt.Printf("Created %s with %d commits\n", path, len(commits)) +} + +// findAuthor returns the first name of the form "L S " +// whose color lies at the given extremes of lightness and saturation, and close +// to the given hue. It also returns the hue that the name lands on. +// +// Every name of this form has the same initials, so the color is the only +// thing that differs between authors in the commits panel. +func findAuthor(lightness extreme, saturation extreme, hue float64) (string, float64) { + for n := 1; ; n++ { + name := fmt.Sprintf("L%s S%s %d", lightness, saturation, n) + h, s, l := authors.ColorPosition(name) + if lightness.matches(l) && saturation.matches(s) && hueDistance(h, hue) <= hueTolerance { + return name, h + } + } +} + +// hueDistance is the distance between two hues on the color wheel, where each +// hue is a fraction of a full turn. +func hueDistance(a float64, b float64) float64 { + d := math.Abs(a - b) + return math.Min(d, 1-d) +} + +func runGit(dir string, env []string, args ...string) error { + cmd := exec.Command("git", args...) + cmd.Dir = dir + // Keep the user's git config out of it, so that hooks, commit signing and + // the like don't apply, and every run creates the same commits + cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_CONFIG_NOSYSTEM=1") + cmd.Env = append(cmd.Env, env...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/pkg/gui/presentation/authors/authors.go b/pkg/gui/presentation/authors/authors.go index 6d2c4f428..f420044d5 100644 --- a/pkg/gui/presentation/authors/authors.go +++ b/pkg/gui/presentation/authors/authors.go @@ -106,6 +106,8 @@ func trueColorStyle(str string) style.TextStyle { 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 ` and open the repository it creates. func colorAtPosition(hue, saturation, lightness float64) colorful.Color { return colorful.Hsl(hue*360.0, 0.6+0.4*saturation, 0.4+lightness*0.2) }