diff --git a/docs-master/Config.md b/docs-master/Config.md index 16b6e2b5a..32fbd0a4e 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -302,14 +302,16 @@ gui: showFileIcons: true # How the commit graph is drawn. - # One of: 'classic' (default) | 'detailed' + # One of: 'auto' (default) | 'classic' | 'detailed' # 'detailed' connects the lines to the commit circles, and shows exactly where # branches fork off and merge. It draws the graph with the git branch drawing # symbols (U+F5D0 to U+F60D), so it needs a terminal that draws these itself: # kitty, Ghostty, WezTerm (nightly builds), Contour, or VS Code's terminal with # GPU acceleration. Other terminals need a font that contains them, such as # https://github.com/rbong/flog-symbols. - commitGraphStyle: classic + # 'auto' uses 'detailed' if lazygit recognizes the terminal as one that draws + # these symbols (kitty and Ghostty), and 'classic' otherwise. + commitGraphStyle: auto # Length of author name in (non-expanded) commits view. 2 means show initials # only. diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index b0279d011..c5e05ed94 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -180,9 +180,10 @@ type GuiConfig struct { // If true (default), file icons are shown in the file views. Only relevant if NerdFontsVersion is not empty. ShowFileIcons bool `yaml:"showFileIcons"` // How the commit graph is drawn. - // One of: 'classic' (default) | 'detailed' + // One of: 'auto' (default) | 'classic' | 'detailed' // 'detailed' connects the lines to the commit circles, and shows exactly where branches fork off and merge. It draws the graph with the git branch drawing symbols (U+F5D0 to U+F60D), so it needs a terminal that draws these itself: kitty, Ghostty, WezTerm (nightly builds), Contour, or VS Code's terminal with GPU acceleration. Other terminals need a font that contains them, such as https://github.com/rbong/flog-symbols. - CommitGraphStyle string `yaml:"commitGraphStyle" jsonschema:"enum=classic,enum=detailed"` + // 'auto' uses 'detailed' if lazygit recognizes the terminal as one that draws these symbols (kitty and Ghostty), and 'classic' otherwise. + CommitGraphStyle string `yaml:"commitGraphStyle" jsonschema:"enum=auto,enum=classic,enum=detailed"` // Length of author name in (non-expanded) commits view. 2 means show initials only. CommitAuthorShortLength int `yaml:"commitAuthorShortLength"` // Length of author name in expanded commits view. 2 means show initials only. @@ -926,7 +927,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { ShowIcons: false, NerdFontsVersion: "", ShowFileIcons: true, - CommitGraphStyle: "classic", + CommitGraphStyle: "auto", CommitAuthorShortLength: 2, CommitAuthorLongLength: 17, CommitHashLength: 8, diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index d733a9577..cf5c85966 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -31,7 +31,7 @@ func (config *UserConfig) Validate() error { return err } if err := validateEnum("gui.commitGraphStyle", config.Gui.CommitGraphStyle, - []string{"classic", "detailed"}); err != nil { + []string{"auto", "classic", "detailed"}); err != nil { return err } if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches, diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 87e7fb031..12968fa95 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -733,6 +733,12 @@ func (g *Gui) DetectedColorScheme() DetectedColorScheme { return g.colorScheme } +// Terminal returns the name and version that the terminal reported at +// startup. Both are empty if it didn't report them. +func (g *Gui) Terminal() (string, string) { + return g.screen.Terminal() +} + // SetColorSchemeChangeHandler sets a function to call on the UI thread whenever // the terminal's colors change after startup. func (g *Gui) SetColorSchemeChangeHandler(handler func(DetectedColorScheme) error) { diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index 4259da43d..6f3049f24 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -352,8 +352,13 @@ func shouldShowGraph(c *ContextCommon) bool { } func commitGraphSymbolSet(c *ContextCommon) graph.SymbolSet { - if c.UserConfig().Gui.CommitGraphStyle == "detailed" { + switch c.UserConfig().Gui.CommitGraphStyle { + case "detailed": return graph.BranchDrawingSymbols + case "auto": + if graph.TerminalDrawsBranchDrawingSymbols(c.GocuiGui().Terminal()) { + return graph.BranchDrawingSymbols + } } return graph.BoxDrawingSymbols } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 8d66e99b1..3f6233fa7 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -934,6 +934,8 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error { g.ErrorHandler = gui.PopupHandler.ErrorHandler + terminalName, terminalVersion := g.Terminal() + gui.c.Log.Infof("Terminal: %s %s", terminalName, terminalVersion) 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) diff --git a/pkg/gui/presentation/graph/branch_drawing.go b/pkg/gui/presentation/graph/branch_drawing.go index 2e3537498..60271ad76 100644 --- a/pkg/gui/presentation/graph/branch_drawing.go +++ b/pkg/gui/presentation/graph/branch_drawing.go @@ -1,5 +1,11 @@ package graph +import ( + "slices" + "strconv" + "strings" +) + // The branch drawing symbols are characters in the Unicode Private Use Area // (U+F5D0 to U+F60D) for drawing git graphs, introduced by kitty in // https://github.com/kovidgoyal/kitty/pull/7681. Unlike the box drawing @@ -95,3 +101,32 @@ func (cell *Cell) branchDrawingChars() (string, string) { return first, second } } + +// The terminals that draw the branch drawing symbols themselves, with the +// first version that draws all of them +var terminalsWithBranchDrawingSymbols = map[string][]int{ + "kitty": {0, 36, 2}, + "ghostty": {1, 0, 0}, +} + +// TerminalDrawsBranchDrawingSymbols says whether a terminal is known to draw +// the branch drawing symbols itself, given the name and version it reports +func TerminalDrawsBranchDrawingSymbols(name, version string) bool { + minVersion, ok := terminalsWithBranchDrawingSymbols[strings.ToLower(name)] + return ok && slices.Compare(versionNumbers(version), minVersion) >= 0 +} + +// The numbers at the start of the dot-separated parts of a version, e.g. +// [1 3 0] for "1.3.0-dev+abc" +func versionNumbers(version string) []int { + var numbers []int + for part := range strings.SplitSeq(version, ".") { + digits := part[:len(part)-len(strings.TrimLeft(part, "0123456789"))] + number, err := strconv.Atoi(digits) + if err != nil { + break + } + numbers = append(numbers, number) + } + return numbers +} diff --git a/pkg/gui/presentation/graph/branch_drawing_test.go b/pkg/gui/presentation/graph/branch_drawing_test.go new file mode 100644 index 000000000..651b31d23 --- /dev/null +++ b/pkg/gui/presentation/graph/branch_drawing_test.go @@ -0,0 +1,33 @@ +package graph + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTerminalDrawsBranchDrawingSymbols(t *testing.T) { + tests := []struct { + name string + version string + expected bool + }{ + {name: "kitty", version: "0.36.1", expected: false}, + {name: "kitty", version: "0.36.2", expected: true}, + {name: "kitty", version: "0.44.0", expected: true}, + {name: "ghostty", version: "0.9.0", expected: false}, + {name: "ghostty", version: "1.0.0", expected: true}, + {name: "ghostty", version: "1.3.0-main+0123abcd", expected: true}, + {name: "WezTerm", version: "20250601-102030-89abcdef", expected: false}, + {name: "tmux", version: "3.5a", expected: false}, + {name: "iTerm2", version: "3.6.4", expected: false}, + {name: "kitty", version: "", expected: false}, + {name: "", version: "", expected: false}, + } + + for _, test := range tests { + t.Run(test.name+" "+test.version, func(t *testing.T) { + assert.Equal(t, test.expected, TerminalDrawsBranchDrawingSymbols(test.name, test.version)) + }) + } +} diff --git a/schema-master/config.json b/schema-master/config.json index d93f1450a..88fb11f79 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -1224,11 +1224,12 @@ "commitGraphStyle": { "type": "string", "enum": [ + "auto", "classic", "detailed" ], - "description": "How the commit graph is drawn.\nOne of: 'classic' (default) | 'detailed'\n'detailed' connects the lines to the commit circles, and shows exactly where branches fork off and merge. It draws the graph with the git branch drawing symbols (U+F5D0 to U+F60D), so it needs a terminal that draws these itself: kitty, Ghostty, WezTerm (nightly builds), Contour, or VS Code's terminal with GPU acceleration. Other terminals need a font that contains them, such as https://github.com/rbong/flog-symbols.", - "default": "classic" + "description": "How the commit graph is drawn.\nOne of: 'auto' (default) | 'classic' | 'detailed'\n'detailed' connects the lines to the commit circles, and shows exactly where branches fork off and merge. It draws the graph with the git branch drawing symbols (U+F5D0 to U+F60D), so it needs a terminal that draws these itself: kitty, Ghostty, WezTerm (nightly builds), Contour, or VS Code's terminal with GPU acceleration. Other terminals need a font that contains them, such as https://github.com/rbong/flog-symbols.\n'auto' uses 'detailed' if lazygit recognizes the terminal as one that draws these symbols (kitty and Ghostty), and 'classic' otherwise.", + "default": "auto" }, "commitAuthorShortLength": { "type": "integer", diff --git a/test/default_test_config/config.yml b/test/default_test_config/config.yml index 198fcbdd1..e5a893a13 100644 --- a/test/default_test_config/config.yml +++ b/test/default_test_config/config.yml @@ -12,6 +12,9 @@ gui: - black # Not important in tests but it creates clutter in demos showRandomTip: false + # The expected output of the tests draws the commit graph with box drawing + # characters, whichever terminal a test runs in + commitGraphStyle: classic animateExplosion: false # takes too long git: # We don't want to run any periodic background git commands because it'll introduce race conditions and flakiness.