mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
230 lines
9.1 KiB
Go
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))
|
|
}
|