mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 02:07:09 -04:00
Allow telling diff renderers whether the terminal is dark or light (#6063)
Renderers like delta and difftastic pick their colors for either a dark
or a light background, and they can't find out which one the terminal
has, because lazygit runs them with TERM=dumb, and in a pty that doesn't
answer their queries.
Add `{{colorScheme}}` to the commands of diff renderers. It is 'dark' or
'light', based on what the terminal reports (which can be overridden by
`gui.colorScheme` if the terminal doesn't support the query). It can be
passed to delta as `--{{colorScheme}}` and to difftastic as
`--background={{colorScheme}}`; other renderers can choose between
options with a template expression.
This commit is contained in:
@@ -23,22 +23,29 @@ Fields only for `extDiff`:
|
||||
|
||||
- **command** The command line to use for the `diff.external` git config. If left empty, it uses the global value of git's `diff.external` config; this can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
|
||||
|
||||
You can include the `{{diffContext}}` template variable to pass lazygit's current diff context size (the value controlled by the `{`/`}` keybindings) to the diff tool.
|
||||
|
||||
Fields only for `rawGit`:
|
||||
|
||||
- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`), as an array of strings.
|
||||
|
||||
The `command` of a `stdinFilter` or `extDiff` renderer is a [Go template](https://pkg.go.dev/text/template) with these variables:
|
||||
|
||||
- `{{width}}`: the width of the view that the diff is rendered into.
|
||||
- `{{colorScheme}}`: `dark` or `light`, depending on whether the terminal has a dark or a light background. Lazygit asks the terminal about this; if yours doesn't tell, set `gui.colorScheme`.
|
||||
- `{{columnWidth}}` (only for `stdinFilter`): the width of one side of a side-by-side rendering, e.g. for `ydiff -p cat -s -w {{columnWidth}}`.
|
||||
- `{{diffContext}}` (only for `extDiff`): lazygit's current diff context size, the value controlled by the `{`/`}` keybindings.
|
||||
|
||||
A variable can also be written with a leading dot, as in `{{.width}}`. The command can use template expressions too; for example, `delta --paging=never {{if gt .width 160}}--side-by-side{{end}}` shows the diff side by side only when there is room for it, or `delta --syntax-theme={{if eq .colorScheme "light"}}Github{{else}}Dracula{{end}}` picks a different syntax theme based on the background.
|
||||
|
||||
Here's an example for a multi-renderer setup:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
diffRenderers:
|
||||
- command: delta --dark --paging=never
|
||||
- command: delta --{{colorScheme}} --paging=never
|
||||
- command: ydiff -p cat
|
||||
colorArg: never
|
||||
- type: extDiff
|
||||
command: difft --color=always --context={{diffContext}}
|
||||
command: difft --color=always --background={{colorScheme}} --context={{diffContext}}
|
||||
- type: rawGit
|
||||
args: [--color-words]
|
||||
name: color-words
|
||||
@@ -51,12 +58,14 @@ git:
|
||||
```yaml
|
||||
git:
|
||||
diffRenderers:
|
||||
- command: delta --dark --paging=never
|
||||
- command: delta --{{colorScheme}} --paging=never
|
||||
```
|
||||
|
||||

|
||||
|
||||
A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `command:` field to `delta --dark --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
|
||||
`--{{colorScheme}}` passes `--dark` or `--light` to delta, so that it matches the background of your terminal.
|
||||
|
||||
A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `command:` field to `delta --{{colorScheme}} --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
|
||||
|
||||
Note that delta's `--navigate` option doesn't work in lazygit, for technical reasons.
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -60,19 +61,23 @@ func (self *DiffRendererConfigManager) GetDiffRendererType() DiffRendererType {
|
||||
return currentDiffRendererConfig.getType()
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetStdinFilterCommand(width int) string {
|
||||
// DiffRendererValues are what the command of a diff renderer can refer to.
|
||||
type DiffRendererValues struct {
|
||||
// The width of the view that the diff is rendered into
|
||||
Width int
|
||||
// The number of lines of context around each hunk
|
||||
DiffContext uint64
|
||||
// Whether the terminal has a light background
|
||||
LightBackground bool
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetStdinFilterCommand(values DiffRendererValues) (string, error) {
|
||||
currentDiffRendererConfig := self.currentDiffRendererConfig()
|
||||
if currentDiffRendererConfig == nil || currentDiffRendererConfig.getType() != DiffRendererType_StdinFilter {
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
templateValues := map[string]string{
|
||||
"width": strconv.Itoa(width),
|
||||
"columnWidth": strconv.Itoa(width/2 - 6),
|
||||
}
|
||||
|
||||
commandTemplate := string(currentDiffRendererConfig.Command)
|
||||
return utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
return currentDiffRendererConfig.resolveCommand(values)
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetColorArg() string {
|
||||
@@ -88,18 +93,46 @@ func (self *DiffRendererConfigManager) GetColorArg() string {
|
||||
return colorArg
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetExternalDiffCommand(diffContext uint64, width int) string {
|
||||
func (self *DiffRendererConfigManager) GetExternalDiffCommand(values DiffRendererValues) (string, error) {
|
||||
currentDiffRendererConfig := self.currentDiffRendererConfig()
|
||||
if currentDiffRendererConfig == nil || currentDiffRendererConfig.getType() != DiffRendererType_ExtDiff {
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
templateValues := map[string]string{
|
||||
"diffContext": strconv.Itoa(int(diffContext)),
|
||||
"width": strconv.Itoa(width),
|
||||
return currentDiffRendererConfig.resolveCommand(values)
|
||||
}
|
||||
|
||||
// resolveCommand resolves the renderer's command, which is a Go template with
|
||||
// the values it can refer to as its variables. A variable can be written with
|
||||
// or without the leading dot, as in {{.width}} or {{width}}.
|
||||
func (self *DiffRendererConfig) resolveCommand(values DiffRendererValues) (string, error) {
|
||||
colorScheme := "dark"
|
||||
if values.LightBackground {
|
||||
colorScheme = "light"
|
||||
}
|
||||
variables := map[string]any{
|
||||
"width": values.Width,
|
||||
"colorScheme": colorScheme,
|
||||
}
|
||||
switch self.getType() {
|
||||
case DiffRendererType_StdinFilter:
|
||||
variables["columnWidth"] = values.Width/2 - 6
|
||||
case DiffRendererType_ExtDiff:
|
||||
variables["diffContext"] = values.DiffContext
|
||||
case DiffRendererType_RawGit:
|
||||
// has no command
|
||||
}
|
||||
|
||||
return utils.ResolvePlaceholderString(string(currentDiffRendererConfig.Command), templateValues)
|
||||
funcs := template.FuncMap{}
|
||||
for name, value := range variables {
|
||||
funcs[name] = func() any { return value }
|
||||
}
|
||||
|
||||
command, err := utils.ResolveTemplate(string(self.Command), variables, funcs)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git.diffRenderers: can't use the command '%s': %w", self.Command, err)
|
||||
}
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetRawGitArgs() []string {
|
||||
|
||||
@@ -68,7 +68,9 @@ func TestGetStdinFilterCommand(t *testing.T) {
|
||||
name string
|
||||
diffRendererConfig DiffRendererConfig
|
||||
width int
|
||||
lightBackground bool
|
||||
expected string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "a command without template variables is passed through",
|
||||
@@ -94,6 +96,44 @@ func TestGetStdinFilterCommand(t *testing.T) {
|
||||
width: 120,
|
||||
expected: "delta --width=120",
|
||||
},
|
||||
{
|
||||
name: "the color scheme on a dark background",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --{{colorScheme}}"},
|
||||
width: 120,
|
||||
expected: "delta --dark",
|
||||
},
|
||||
{
|
||||
name: "the color scheme on a light background",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --{{colorScheme}}"},
|
||||
width: 120,
|
||||
lightBackground: true,
|
||||
expected: "delta --light",
|
||||
},
|
||||
{
|
||||
name: "the command can choose between options by the color scheme",
|
||||
diffRendererConfig: DiffRendererConfig{Command: `delta --syntax-theme={{if eq .colorScheme "light"}}GitHub{{else}}Dracula{{end}}`},
|
||||
width: 120,
|
||||
lightBackground: true,
|
||||
expected: "delta --syntax-theme=GitHub",
|
||||
},
|
||||
{
|
||||
name: "the command can use template expressions",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta{{if gt .width 100}} --side-by-side{{end}}"},
|
||||
width: 120,
|
||||
expected: "delta --side-by-side",
|
||||
},
|
||||
{
|
||||
name: "an unknown template variable is an error",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --width={{.widht}}"},
|
||||
width: 120,
|
||||
expectedError: "can't use the command 'delta --width={{.widht}}'",
|
||||
},
|
||||
{
|
||||
name: "an unknown template variable without a leading dot is an error too",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --width={{widht}}"},
|
||||
width: 120,
|
||||
expectedError: "can't use the command 'delta --width={{widht}}'",
|
||||
},
|
||||
{
|
||||
name: "nothing is returned for a renderer of another type",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{width}}"},
|
||||
@@ -108,7 +148,13 @@ func TestGetStdinFilterCommand(t *testing.T) {
|
||||
userConfig.Git.DiffRenderers = []DiffRendererConfig{s.diffRendererConfig}
|
||||
config := NewDiffRendererConfigManager(func() *UserConfig { return userConfig })
|
||||
|
||||
assert.Equal(t, s.expected, config.GetStdinFilterCommand(s.width))
|
||||
command, err := config.GetStdinFilterCommand(DiffRendererValues{Width: s.width, LightBackground: s.lightBackground})
|
||||
if s.expectedError != "" {
|
||||
assert.ErrorContains(t, err, s.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expected, command)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -118,6 +164,7 @@ func TestGetExternalDiffCommand(t *testing.T) {
|
||||
name string
|
||||
diffRendererConfig DiffRendererConfig
|
||||
expected string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "a command without template variables is passed through",
|
||||
@@ -129,11 +176,21 @@ func TestGetExternalDiffCommand(t *testing.T) {
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{width}}"},
|
||||
expected: "difft --width=120",
|
||||
},
|
||||
{
|
||||
name: "the color scheme",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --background={{colorScheme}}"},
|
||||
expected: "difft --background=dark",
|
||||
},
|
||||
{
|
||||
name: "the width alongside the diff context size",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{width}} --context={{diffContext}}"},
|
||||
expected: "difft --width=120 --context=3",
|
||||
},
|
||||
{
|
||||
name: "a variable of stdin filters is an error",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{columnWidth}}"},
|
||||
expectedError: "can't use the command 'difft --width={{columnWidth}}'",
|
||||
},
|
||||
{
|
||||
name: "nothing is returned for a renderer of another type",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --width={{width}}"},
|
||||
@@ -147,7 +204,13 @@ func TestGetExternalDiffCommand(t *testing.T) {
|
||||
userConfig.Git.DiffRenderers = []DiffRendererConfig{s.diffRendererConfig}
|
||||
config := NewDiffRendererConfigManager(func() *UserConfig { return userConfig })
|
||||
|
||||
assert.Equal(t, s.expected, config.GetExternalDiffCommand(3, 120))
|
||||
command, err := config.GetExternalDiffCommand(DiffRendererValues{Width: 120, DiffContext: 3})
|
||||
if s.expectedError != "" {
|
||||
assert.ErrorContains(t, err, s.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expected, command)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,10 +124,16 @@ func validateDiffRenderers(diffRenderers []DiffRendererConfig) error {
|
||||
if len(diffRenderer.Args) > 0 {
|
||||
return errors.New("git.diffRenderers: 'args' cannot be used with diff renderer type 'stdinFilter'.")
|
||||
}
|
||||
if err := validateDiffRendererCommand(diffRenderer); err != nil {
|
||||
return err
|
||||
}
|
||||
case "extDiff":
|
||||
if len(diffRenderer.Args) > 0 {
|
||||
return errors.New("git.diffRenderers: 'args' cannot be used with diff renderer type 'extDiff'.")
|
||||
}
|
||||
if err := validateDiffRendererCommand(diffRenderer); err != nil {
|
||||
return err
|
||||
}
|
||||
case "rawGit":
|
||||
if diffRenderer.Command != "" {
|
||||
return errors.New("git.diffRenderers: 'command' cannot be used with diff renderer type 'rawGit'.")
|
||||
@@ -139,6 +145,14 @@ func validateDiffRenderers(diffRenderers []DiffRendererConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDiffRendererCommand resolves the command with made-up values, so that
|
||||
// a mistake in it shows up when the config is loaded rather than when a diff
|
||||
// is rendered.
|
||||
func validateDiffRendererCommand(diffRenderer DiffRendererConfig) error {
|
||||
_, err := diffRenderer.resolveCommand(DiffRendererValues{Width: 80, DiffContext: 3})
|
||||
return err
|
||||
}
|
||||
|
||||
func validateEnum(name string, value string, allowedValues []string) error {
|
||||
if slices.Contains(allowedValues, value) {
|
||||
return nil
|
||||
|
||||
@@ -409,9 +409,14 @@ func TestUserConfigValidate_diffRenderers(t *testing.T) {
|
||||
{name: "stdinFilter with explicit type", diffRenderer: DiffRendererConfig{Type: "stdinFilter"}, valid: false},
|
||||
{name: "stdinFilter with type default without command", diffRenderer: DiffRendererConfig{}, valid: false},
|
||||
{name: "stdinFilter with args", diffRenderer: DiffRendererConfig{Type: "stdinFilter", Command: "delta", Args: []string{"-x"}}, valid: false},
|
||||
{name: "stdinFilter with a template", diffRenderer: DiffRendererConfig{Command: "delta --width={{width}}{{if gt .width 100}} --side-by-side{{end}}"}, valid: true},
|
||||
{name: "stdinFilter with an unknown template variable", diffRenderer: DiffRendererConfig{Command: "delta --width={{.widht}}"}, valid: false},
|
||||
{name: "stdinFilter with a broken template", diffRenderer: DiffRendererConfig{Command: "delta {{if .width}}"}, valid: false},
|
||||
{name: "external diff", diffRenderer: DiffRendererConfig{Type: "extDiff", Command: "difft"}, valid: true},
|
||||
{name: "external diff without command", diffRenderer: DiffRendererConfig{Type: "extDiff"}, valid: true},
|
||||
{name: "external diff with args", diffRenderer: DiffRendererConfig{Type: "extDiff", Command: "difft", Args: []string{"-x"}}, valid: false},
|
||||
{name: "external diff with a template", diffRenderer: DiffRendererConfig{Type: "extDiff", Command: "difft --context={{diffContext}}"}, valid: true},
|
||||
{name: "external diff with a variable of stdin filters", diffRenderer: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{columnWidth}}"}, valid: false},
|
||||
{name: "raw git", diffRenderer: DiffRendererConfig{Type: "rawGit"}, valid: true},
|
||||
{name: "raw git with args", diffRenderer: DiffRendererConfig{Type: "rawGit", Args: []string{"-x"}}, valid: true},
|
||||
{name: "raw git with command", diffRenderer: DiffRendererConfig{Type: "rawGit", Command: "delta"}, valid: false},
|
||||
|
||||
@@ -3,7 +3,6 @@ package controllers
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -185,13 +184,7 @@ func (self *GlobalController) cycleDiffRenderersBackward() error {
|
||||
// onDiffRenderersChanged re-renders the main view so the newly selected diff renderer
|
||||
// takes effect, and shows a toast naming it.
|
||||
func (self *GlobalController) onDiffRenderersChanged() {
|
||||
currentSide := self.c.Context().CurrentSide()
|
||||
currentKey := self.c.Context().Current().GetKey()
|
||||
if currentSide.GetKey() == currentKey ||
|
||||
currentKey == context.NORMAL_MAIN_CONTEXT_KEY ||
|
||||
currentKey == context.NORMAL_SECONDARY_CONTEXT_KEY {
|
||||
currentSide.HandleRenderToMain()
|
||||
}
|
||||
self.c.Helpers().Diff.RenderToMainAgain()
|
||||
|
||||
diffRendererConfigManager := self.c.State().GetDiffRendererConfigManager()
|
||||
current, total := diffRendererConfigManager.CurrentDiffRendererIndex()
|
||||
|
||||
@@ -98,6 +98,19 @@ func (self *DiffHelper) ExitDiffMode() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderToMainAgain renders the current side panel into the main view again, if
|
||||
// that is what the main view shows. This is for when something that the
|
||||
// rendering depends on has changed, such as the diff renderer.
|
||||
func (self *DiffHelper) RenderToMainAgain() {
|
||||
currentSide := self.c.Context().CurrentSide()
|
||||
currentKey := self.c.Context().Current().GetKey()
|
||||
if currentSide.GetKey() == currentKey ||
|
||||
currentKey == context.NORMAL_MAIN_CONTEXT_KEY ||
|
||||
currentKey == context.NORMAL_SECONDARY_CONTEXT_KEY {
|
||||
currentSide.HandleRenderToMain()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *DiffHelper) RenderDiff() {
|
||||
args := self.DiffArgs()
|
||||
cmdObj := self.c.Git().Diff.DiffCmdObj(args)
|
||||
|
||||
@@ -948,6 +948,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
||||
gui.applyTerminalBackground()
|
||||
gui.c.Contexts().LocalCommits.HandleRender()
|
||||
gui.c.Contexts().SubCommits.HandleRender()
|
||||
gui.helpers.Diff.RenderToMainAgain()
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -56,8 +57,19 @@ func (gui *Gui) newRenderTask(view *gocui.View, cmd *exec.Cmd, prefix string) er
|
||||
// width to render at known, and with it the renderer command.
|
||||
width := view.InnerWidth()
|
||||
diffRendererConfigManager := gui.stateAccessor.GetDiffRendererConfigManager()
|
||||
stdinFilter := diffRendererConfigManager.GetStdinFilterCommand(width)
|
||||
externalDiff := diffRendererConfigManager.GetExternalDiffCommand(gui.c.UserConfig().Git.DiffContextSize, width)
|
||||
values := config.DiffRendererValues{
|
||||
Width: width,
|
||||
DiffContext: gui.c.UserConfig().Git.DiffContextSize,
|
||||
LightBackground: gui.terminalHasLightBackground(),
|
||||
}
|
||||
stdinFilter, stdinFilterErr := diffRendererConfigManager.GetStdinFilterCommand(values)
|
||||
externalDiff, externalDiffErr := diffRendererConfigManager.GetExternalDiffCommand(values)
|
||||
if err := errors.Join(stdinFilterErr, externalDiffErr); err != nil {
|
||||
// The commands are checked with made-up values when the config is
|
||||
// loaded, so only one that fails for some values and not for others
|
||||
// gets here. Git's own diff is shown instead.
|
||||
gui.c.ErrorToast(err.Error())
|
||||
}
|
||||
|
||||
cmdStr := strings.Join(cmd.Args, " ")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user