Files
lazygit/pkg/gui/main_view_render.go
T

230 lines
9.1 KiB
Go

package gui
import (
"errors"
"fmt"
"io"
"os/exec"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/tasks"
)
// renderSpec describes a render of a command's output into a view: what a way
// of running the command (see runRender) needs to know about it.
type renderSpec struct {
view *gocui.View
cmd *exec.Cmd
// The width the renderer lays its rendering out to, and the width the view
// counts its own wrapping against. Read after the layout pass, which
// settles it.
width int
// The configured stdin filter, empty unless one is configured. How git gets
// to run it depends on the way the render runs. An external diff renderer
// is named to git in the environment before the render is set up, whichever
// way it runs, so it doesn't appear here; nothing else is a command of its
// own.
stdinFilter string
}
// newRenderTask renders cmd's output into view, through the diff renderer the
// user has configured. The renderer lays its rendering out to the width of the
// view, which only the layout settles, so the task is created after it.
func (gui *Gui) newRenderTask(view *gocui.View, cmd *exec.Cmd, prefix types.Prefix) error {
// Ask whatever renders the diff to state, in an OSC 1717 record per line,
// which line of which file it is rendering. This lets us act on the line the
// user is pointing at even when the rendering no longer looks like a diff.
// The variable names the protocol versions we understand, and a renderer
// that doesn't understand it ignores it, so we can set it always. It has to
// be set before the plain path below, since on that path git renders the
// diff itself, and git speaks the protocol too, for its word-diff formats,
// whose markup we could not otherwise resolve.
cmd.Env = append(cmd.Env, "OSC1717=V1")
if gui.stateAccessor.GetDiffRendererConfigManager().GetDiffRendererType() == config.DiffRendererType_RawGit {
// If we're not using a custom diff renderer, then we don't need to use a pty
return gui.newCmdTask(view, cmd, prefix)
}
// The key the render is remembered under says which diff it is of, so that a
// re-render of the same diff can be told from a render of another one. Take
// it before anything else can touch the command's arguments.
cmdStr := strings.Join(cmd.Args, " ")
manager := gui.getManager(view)
// The task takes its place among the view's tasks now, although it is only
// created after the layout (see the matching call in newCmdTask).
reservation := manager.ReserveTask()
// Mark the view as loading synchronously now, before the layout pass: the
// actual task is created in afterLayout (below), which runs after layout, so
// without this the next layout pass would clamp the scroll position to the
// not-yet-loaded content.
manager.StartLoading()
// Hold the scrollbar at its current height while the re-render loads, so the
// thumb doesn't shrink and snap back when the first partial paint swaps in
// (see the matching call in newCmdTask).
view.FreezeScrollbarHeight()
// Run the render after layout so that it gets the correct size
gui.afterLayout(func() error {
if manager.IsSuperseded(reservation) {
return nil
}
// The layout may have changed the size of the view, so only now is the
// width to render at known, and with it the renderer command.
width := gui.renderWidth(view)
diffRendererConfigManager := gui.stateAccessor.GetDiffRendererConfigManager()
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())
}
oscommands.SetDumbTerminalEnv(cmd)
// An external diff command is named to git here, in the environment,
// because the width it renders at is only known after the layout, and
// the command's arguments were settled before it. An empty command
// means the user wants git's own diff.external config to apply, so
// leave the variable unset in that case; git takes it being set at all
// as an instruction, however little it says.
if externalDiff != "" {
cmd.Env = append(cmd.Env, "GIT_EXTERNAL_DIFF="+externalDiff)
}
spec := renderSpec{
view: view,
cmd: cmd,
width: width,
stdinFilter: stdinFilter,
}
run := gui.ptyRender
if oscommands.RendersThroughAPipe() {
run = gui.pipedRender
}
return gui.newTaskForRender(reservation, spec, prefix, cmdStr, run)
})
return nil
}
// renderWidth is the width a render into view is laid out to: the view's own, less the
// columns the custom patch's marks take from it where they are drawn over the render
// (see DiffLineHelper.ShowsInclusionGutter).
func (gui *Gui) renderWidth(view *gocui.View) int {
width := view.InnerWidth()
if gui.helpers.DiffLine.ShowsInclusionGutter(view) {
width -= view.InclusionGutterWidthWhenShown()
}
return max(0, width)
}
// The start and onClose functions a render hands to its task: how to get the
// command running and the output reader for it, and how to tear it down again
// once the task is stopped.
type (
startRender func() (tasks.Cmd, io.Reader)
onCloseRender func()
)
// runRender is a way of running a render's command and getting at its output:
// plainly, in a pty, or through a pipe with the stdin filter as a command of our
// own. It returns the functions the task drives the command by.
type runRender func(spec renderSpec) (startRender, onCloseRender)
// newTaskForRender creates the task that reads the render's output into its
// view, running the command the given way. The task takes the place that the
// reservation holds among the view's tasks. key names what is rendered, so that
// a re-render of the same content can be told from a render of other content.
func (gui *Gui) newTaskForRender(reservation tasks.TaskReservation, spec renderSpec, prefix types.Prefix, key string, run runRender) error {
setColumnsEnvVar(spec.cmd, spec.width)
start, onClose := run(spec)
// The prefix is laid out here, on the UI thread, now that the width is
// known; its text is produced on the task's goroutine.
var producePrefix func() string
if prefix != nil {
producePrefix = prefix(spec.width)
}
manager := gui.getManager(spec.view)
linesToRead := gui.linesToReadFromCmdTask(spec.view)
return manager.NewReservedTask(reservation, manager.NewCmdTask(start, producePrefix, linesToRead, onClose), key)
}
// pipedRender feeds the diff renderer the command's output through a pipe.
//
// A stdin filter becomes a command of our own here, because git only invokes
// the one named by GIT_PAGER when it thinks it is talking to a terminal, so
// with a pipe the filter would never run. An external diff renderer is git's
// own business, named in the environment and run by git per file, so with one
// the command runs alone.
func (gui *Gui) pipedRender(spec renderSpec) (startRender, onCloseRender) {
if spec.stdinFilter == "" {
return gui.plainRender(spec)
}
view := spec.view
cmd := spec.cmd
var pipe io.ReadCloser
start := func() (tasks.Cmd, io.Reader) {
// See the matching call in ptyRender for why this happens here.
view.SetContentWidth(spec.width)
// The filter runs in a plain shell, without lazygit's shell functions
// sourced, since that is the shell git would have run it in. It is
// handed git's environment for the same reason: as git's child it
// would have inherited exactly that.
pipeline, reader, err := gui.os.StartPipeline(
gui.os.Cmd.NewFromCmd(cmd).DontLog(),
gui.os.Cmd.NewShell(spec.stdinFilter, "").SetEnviron(cmd.Env).DontLog(),
)
if err != nil {
gui.c.Log.Error(err)
// The command has been started and stopped again by now, so it
// can't be run a second time without the renderer. Show what went
// wrong where the diff would have been.
return tasks.ExecCmd{Cmd: cmd}, strings.NewReader(err.Error())
}
pipe = reader
return pipeline, reader
}
onClose := func() {
// Closing the reader brings the pipeline down. The renderer's next write
// fails, so it exits, and git's write into the pipe the renderer was
// reading fails in turn.
if pipe != nil {
pipe.Close()
pipe = nil
}
}
return start, onClose
}
// setColumnsEnvVar tells a command how wide the view its output goes into is.
// git reads COLUMNS in preference to the size of the terminal it is talking to,
// and lays the diffstat graph out to it; a diff renderer with no terminal to
// ask may read it too (difftastic and diff-so-fancy do, delta does not). A
// command told nothing renders for 80 columns.
func setColumnsEnvVar(cmd *exec.Cmd, width int) {
cmd.Env = append(cmd.Env, fmt.Sprintf("COLUMNS=%d", width))
}