Ctrl K
ComunidadGuías…

pkg/tasks/tasks.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package tasks

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strconv"
	"sync"
	"sync/atomic"
	"time"

	"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
	"github.com/jesseduffield/lazygit/pkg/gocui"
	"github.com/jesseduffield/lazygit/pkg/utils"
	"github.com/sasha-s/go-deadlock"
	"github.com/sirupsen/logrus"
)

// Cmd abstracts over a started external process. *exec.Cmd satisfies the bulk
// of it via ExecCmd, but pty implementations can supply their own types — on
// Windows, ConPTY has to spawn via CreateProcess directly and can't use
// *exec.Cmd (see golang/go#62708).
type Cmd interface {
	Wait() error
	String() string
	// Terminate makes the process stop early, as gracefully as the platform
	// allows. It doesn't wait for the process to exit.
	Terminate() error
}

// ExecCmd adapts *exec.Cmd to Cmd.
type ExecCmd struct {
	*exec.Cmd
}

// Terminate sends SIGTERM on Unix. On Windows it does nothing, so a stopped
// command keeps running until it next writes to its (by then closed) output
// pipe.
func (c ExecCmd) Terminate() error {
	return oscommands.TerminateProcessGracefully(c.Process)
}

// This file revolves around running commands that will be output to the main panel
// in the gui. If we're flicking through the commits panel, we want to invoke a
// `git show` command for each commit, but we don't want to read the entire output
// at once (because that would slow things down); we just want to fill the panel
// and then read more as the user scrolls down. We also want to ensure that we're only
// ever running one `git show` command at time, and that we only have one command
// writing its output to the main panel at a time.

const THROTTLE_TIME = time.Millisecond * 30

// we use this to check if the system is under stress right now. Hopefully this makes sense on other machines
const COMMAND_START_THRESHOLD = time.Millisecond * 10

type ViewBufferManager struct {
	// this blocks until the task has been properly stopped
	stopCurrentTask func()

	// this is what we write the output of the task to. It's typically a view
	writer io.Writer

	waitingMutex deadlock.Mutex
	// Guards newTaskID and taskKey, which identify the most recently requested
	// task. newTaskID is written wherever a task is asked for (ReserveTask) and
	// read on the goroutine NewReservedTask spawns. taskKey is written on that
	// goroutine and read from the UI thread (GetTaskKey). So neither may be
	// touched without holding this.
	taskIDMutex deadlock.Mutex
	Log         *logrus.Entry
	newTaskID   int
	// The requests by which the currently-running task is told to read more lines
	// (e.g. as the user scrolls), and which it answers once it has. The task
	// serving them comes and goes; see readRequestQueue.
	readRequests *readRequestQueue
	taskKey      string

	// Resets the view's scroll position to the top. A render whose content is
	// different from what the view last showed (a different command key) calls
	// this — but at its *first paint*, not when the task starts: the off-screen
	// render leaves the previous content displayed until the swap, so resetting
	// the origin up front would scroll that still-displayed content to the top
	// before the new content replaces it. See newContentPending.
	resetOrigin func()

	// Whether the content the running task is rendering differs from what the
	// view is currently showing (i.e. the command key changed). Two things key
	// off it: the loading indicator only takes the view over when it is set,
	// since there is no point clearing content we are about to render
	// identically; and the first paint that reveals the content resets the
	// scroll to the top and clears it.
	//
	// It deliberately outlives the task that set it: a task can be stopped and
	// replaced before it ever paints — a background refresh landing just after
	// the user clicked a different item, say — and the replacement, which
	// renders the same content and so sets nothing of its own, still has to do
	// what that task was owed.
	newContentPending atomic.Bool

	// When set, the next command task puts the view back where it was once it has
	// re-rendered the content, instead of showing the new render from the top (see
	// RenderRestore). It is installed just before the re-render is triggered.
	//
	// Like newContentPending it outlives the task it was installed for, and for the
	// same reason: that task can be stopped and replaced before it ever paints, and
	// the replacement, rendering the same content, is then the one that owes the
	// user their position. It is cleared by whichever task applies it. Guarded by
	// taskIDMutex, like the task key.
	restoreForNextTask *RenderRestore

	// When set, the next command task leaves the view's scroll position alone even
	// though it renders a different command's output, that output being the same
	// content laid out differently (see SetKeepScrollPositionForNextTask). The task
	// that starts consumes it, in place of noting that new content is on its way.
	// Guarded by taskIDMutex, like the task key.
	keepScrollForNextTask bool

	// The command task whose output the view is loading: the one that was asked
	// for last when StartLoading was called. It is cleared when that task has
	// read all of its input. The view counts as loading only while this task is
	// still the one asked for last; a task asked for after it takes the view
	// over, whatever it shows. Guarded by taskIDMutex, like the task key.
	loadingTaskID int

	// beforeStart is the function that is called before starting a new task
	beforeStart  func()
	refreshView  func()
	onEndOfInput func()

	// beginRender starts an off-screen render: the new content is built without
	// disturbing what's displayed. swapInRender then promotes it to the display
	// in one step. Together they keep the view showing the previous render until
	// the new one has read enough to paint, instead of revealing it line by line.
	beginRender  func()
	swapInRender func()

	// see docs/dev/Busy.md
	// A gocui task is not the same thing as the tasks defined in this file.
	// A gocui task simply represents the fact that lazygit is busy doing something,
	// whereas the tasks in this file are about rendering content to a view.
	newGocuiTask func() gocui.Task

	// Runs f on the UI thread and blocks until it has completed. All mutations
	// of the view happen through this, so that the view is only ever touched on
	// the UI thread (where it is also laid out and drawn), never on the task's
	// own goroutine.
	onUIThread func(f func()) error

	// if the user flicks through a heap of items, with each one
	// spawning a process to render something to the main view,
	// it can slow things down quite a bit. In these situations we
	// want to throttle the spawning of processes. Atomic because it's set
	// from one task's stop goroutine and read when the next task starts.
	throttle atomic.Bool
}

type LinesToRead struct {
	// The total number of lines the task should have read once this request is
	// satisfied. This is an absolute count from the start of the task, not a
	// delta: the task keeps track of how many lines it has already read and only
	// reads the shortfall, so a request for a total at or below what has already
	// been read reads nothing. -1 means read all the way to the end.
	Total int

	// Number of lines after which we have read enough to fill the view, and can
	// do an initial refresh. Only set for the initial read request; -1 for
	// subsequent requests.
	//
	// Unless Total is -1, it must not be more than Total. The task shows nothing
	// of what it reads until it has read this many lines or has reached the end
	// of its input, so a task that stops reading before then leaves the view
	// showing the previous render.
	InitialRefreshAfter int

	// The number of lines above the view when the task was asked for. Only set
	// for the initial read request, whose Total and InitialRefreshAfter don't
	// count these lines. A task that shows its content where the view is reads
	// them as well, so that it fills the view there. A task that shows content
	// the view isn't showing yet shows it from the top and doesn't read them.
	LinesAboveView int

	// Function to call after reading the lines is done
	Then func()
}

// includingLinesAboveView returns the initial read request for a task that shows its
// content where the view is rather than from the top. Such a task reads the lines
// above the view as well.
func (self LinesToRead) includingLinesAboveView() LinesToRead {
	if self.Total != -1 {
		self.Total += self.LinesAboveView
	}
	if self.InitialRefreshAfter != -1 {
		self.InitialRefreshAfter += self.LinesAboveView
	}
	return self
}

// RenderRestore puts a view back where it was when it re-renders content the user
// is already looking at, laid out differently — a different context size, whitespace
// ignored, another diff renderer — instead of showing the new render from the top.
//
// The task reads the new content into an off-screen buffer; the restore says when
// enough of it has arrived to show the remembered position (FirstPaintReady), and
// then finds that position and reveals it (Apply). It is a pair of callbacks rather
// than a scroll position because a different layout of the same content puts the
// remembered line somewhere else, and only the new content itself says where.
type RenderRestore struct {
	// FirstPaintReady reports whether enough of the new content has been read for
	// the restore to show what it is looking for. It is consulted after each line
	// is read, on the task's own goroutine.
	FirstPaintReady func() bool

	// Apply runs once, on the UI thread, at the first paint. It finds its target in
	// the off-screen content, calls swapIn to promote that content to the display,
	// and places the view on the target — in that order, so that the search runs
	// while the previous content is still displayed, and the new content is never
	// drawn at the previous render's scroll position.
	//
	// It must call swapIn even when it finds nothing to place the view on, in which
	// case the view keeps the position the paint gave it: the offset it had, or the
	// top for content the view hasn't seen.
	Apply func(swapIn func())

	// Done is called once the restore has had its render — after Apply, or when it
	// is given up because the view is being shown something other than a re-render
	// of what it was remembered from. It is how a caller that has to wait for the
	// view to be back where it belongs knows that it either is, or never will be.
	// Optional, and called on the UI thread, as Apply is.
	Done func()
}

// resolved reports that this restore's render has happened, or that there will not be
// one. Called on the UI thread, from wherever the restore ends: once, whichever way it
// ended.
func (self *RenderRestore) resolved() {
	if self.Done != nil {
		done := self.Done
		self.Done = nil
		done()
	}
}

// SetRestoreForNextTask arranges for the next command task to put the view back
// where it is now once it has re-rendered. Call it right before triggering a
// re-render of the content the view is showing; see RenderRestore.
func (self *ViewBufferManager) SetRestoreForNextTask(restore *RenderRestore) {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	self.restoreForNextTask = restore
}

// HasRestoreForNextTask reports whether the next command task already has a position
// waiting to be put back, for a caller that would otherwise install one of its own
// over it.
func (self *ViewBufferManager) HasRestoreForNextTask() bool {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	return self.restoreForNextTask != nil
}

func (self *ViewBufferManager) getRestoreForNextTask() *RenderRestore {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	return self.restoreForNextTask
}

// SetKeepScrollPositionForNextTask arranges for the next command task to leave the
// view's scroll position alone, rather than showing its content from the top the way a
// render of different content does. Call it right before triggering a re-render of the
// content the view is showing, when the command producing it is not the one that
// produced what is on screen — a different context size, another diff renderer.
//
// It is the coarser sibling of SetRestoreForNextTask, for the same moment. The restore
// puts the view back on the line it remembers, which it can only do when the lines of
// the new rendering can be told apart. This one says merely "the content is a
// rearrangement of what is there, so the offset into it is nearer to where the user was
// than the top is". Both can be set at once, and then the restore has the first say.
func (self *ViewBufferManager) SetKeepScrollPositionForNextTask() {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	self.keepScrollForNextTask = true
}

// clearRestore drops a restore once a task has applied it, so that it rides exactly
// one re-render. One installed since — the user pressing the key again while this
// task was still reading — is left alone: it belongs to the render on its way.
func (self *ViewBufferManager) clearRestore(restore *RenderRestore) {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	if self.restoreForNextTask == restore {
		self.restoreForNextTask = nil
	}
}

// DropRestoreForNextTask gives up a restore that has no render to ride, because the
// view is being given something other than a re-render of the content it was
// remembered from — a message where a diff was. Without this the restore would sit
// there and claim some later render of that view, putting the user somewhere they
// haven't been for a while.
func (self *ViewBufferManager) DropRestoreForNextTask() {
	self.taskIDMutex.Lock()
	restore := self.restoreForNextTask
	self.restoreForNextTask = nil
	self.taskIDMutex.Unlock()

	if restore != nil {
		restore.resolved()
	}
}

func (self *ViewBufferManager) GetTaskKey() string {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	return self.taskKey
}

// ForgetRenderedContent records that the view no longer shows the render whose key it
// is holding, because it has been emptied. The key says what the view is showing, and
// the next task is compared against it to tell whether that task renders something
// new. A view with nothing in it is showing nothing, so whatever comes next is new.
func (self *ViewBufferManager) ForgetRenderedContent() {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	self.taskKey = ""
}

func NewViewBufferManager(
	log *logrus.Entry,
	writer io.Writer,
	beforeStart func(),
	refreshView func(),
	onEndOfInput func(),
	resetOrigin func(),
	beginRender func(),
	swapInRender func(),
	newGocuiTask func() gocui.Task,
	onUIThread func(f func()) error,
) *ViewBufferManager {
	return &ViewBufferManager{
		readRequests: newReadRequestQueue(),
		Log:          log,
		writer:       writer,
		beforeStart:  beforeStart,
		refreshView:  refreshView,
		onEndOfInput: onEndOfInput,
		resetOrigin:  resetOrigin,
		beginRender:  beginRender,
		swapInRender: swapInRender,
		newGocuiTask: newGocuiTask,
		onUIThread:   onUIThread,
	}
}

// ReadLines asks the task to ensure it has read at least totalLines lines in
// total. Because the count is absolute rather than a delta, repeated requests
// (e.g. as the user scrolls down, back up, and down again) don't re-read lines
// that have already been read: the task only ever reads the shortfall.
func (self *ViewBufferManager) ReadLines(totalLines int) {
	// A request with no Then needs no answer, so there is nothing to do when no
	// task is there to take it.
	self.readRequests.enqueue(LinesToRead{Total: totalLines, InitialRefreshAfter: -1})
}

// IsLoading reports whether the task asked for last is a command task that is
// still reading content into the view, meaning the content is still growing.
func (self *ViewBufferManager) IsLoading() bool {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	return self.loadingTaskID != 0 && self.loadingTaskID == self.newTaskID
}

// StartLoading marks the view as loading the output of the task asked for last,
// which must be a command task. Call it right when that task is asked for, before
// its goroutine runs and before the next layout pass, so that the layout doesn't
// clamp the scroll position to the not-yet-loaded content. The view stops loading
// when the task reaches the end of its input, or when another task is asked for.
func (self *ViewBufferManager) StartLoading() {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	self.loadingTaskID = self.newTaskID
}

// finishLoading records that the given task has read all of its input. If the
// view is loading another task's output, that task is still loading.
func (self *ViewBufferManager) finishLoading(taskID int) {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	if self.loadingTaskID == taskID {
		self.loadingTaskID = 0
	}
}

func (self *ViewBufferManager) ReadToEnd(then func()) {
	self.readHoldingATask(-1, then)
}

// ReadLinesAndWait is ReadLines for lines lazygit is itself waiting on, rather than
// reading ahead of the user. It holds a gocui task until they have been read, so
// lazygit doesn't count as idle in the meantime (docs/dev/Busy.md).
func (self *ViewBufferManager) ReadLinesAndWait(totalLines int) {
	self.readHoldingATask(totalLines, nil)
}

// readHoldingATask asks the task to have read totalLines lines in total (-1 for all of
// them) and calls then once it has. The reading happens on the task's own goroutine and
// the caller is waiting on the result, so lazygit must not count as idle in between.
// The task is done only once then has returned, because then typically hands its work
// to the UI thread, and that work counts as busy only once it is enqueued.
func (self *ViewBufferManager) readHoldingATask(totalLines int, then func()) {
	task := self.newGocuiTask()
	answered := func() {
		if then != nil {
			then()
		}
		task.Done()
	}

	request := LinesToRead{Total: totalLines, InitialRefreshAfter: -1, Then: answered}
	if !self.readRequests.enqueue(request) {
		// With no task reading, everything there is to read has been read.
		answered()
	}
}

// stopServingReadRequests stops holding read requests for a task and answers the
// ones still waiting, so that nobody is left waiting for a callback that isn't
// coming.
func (self *ViewBufferManager) stopServingReadRequests() {
	answerReadRequests(self.readRequests.stopServing())
}

// answerReadRequestsIfLatest answers the read requests still waiting once the given
// task has given the view all of its content, unless another task has been asked for
// since. The requests are waiting for that task's content then, so they are left for
// it to serve.
func (self *ViewBufferManager) answerReadRequestsIfLatest(taskID int) {
	self.taskIDMutex.Lock()
	var unanswered []LinesToRead
	if taskID == self.newTaskID {
		unanswered = self.readRequests.stopServing()
	}
	self.taskIDMutex.Unlock()

	answerReadRequests(unanswered)
}

func answerReadRequests(requests []LinesToRead) {
	for _, request := range requests {
		if request.Then != nil {
			request.Then()
		}
	}
}

// NewCmdTask returns a task that renders the output of the command that start
// starts. prefix, unless nil, produces the text shown above that output. It is
// called on the task's goroutine before the command starts, so a prefix that
// takes a while to produce holds up only this task.
func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix func() string, linesToRead LinesToRead, onDoneFn func()) func(TaskOpts) error {
	return func(opts TaskOpts) error {
		var onDoneOnce sync.Once
		var onFirstPageShownOnce sync.Once

		onFirstPageShown := func() {
			onFirstPageShownOnce.Do(func() {
				opts.InitialContentLoaded()
			})
		}

		onDone := func() {
			if onDoneFn != nil {
				onDoneOnce.Do(onDoneFn)
			}
			onFirstPageShown()
		}

		// Whatever position is owed to the user belongs to this render: it was
		// remembered just before the re-render that led here was triggered.
		restore := self.getRestoreForNextTask()

		// Content the view isn't showing yet is shown from the top (see
		// newContentPending), and content the view is showing is shown where the
		// view is.
		initialRequest := linesToRead
		if !self.newContentPending.Load() {
			initialRequest = linesToRead.includingLinesAboveView()
		}

		if self.throttle.Load() {
			self.Log.Info("throttling task")
			time.Sleep(THROTTLE_TIME)
		}

		stopped := func() bool {
			select {
			case <-opts.Stop:
				return true
			default:
				return false
			}
		}

		if stopped() {
			onDone()
			return nil
		}

		prefixText := ""
		if prefix != nil {
			prefixText = prefix()

			// A task stopped while it was producing its prefix has no use for the
			// command's output any more.
			if stopped() {
				onDone()
				return nil
			}
		}

		startTime := time.Now()
		cmd, r := start()
		timeToStart := time.Since(startTime)

		done := make(chan struct{})

		go utils.Safe(func() {
			select {
			case <-done:
				// The command finished and did not have to be preemptively stopped before the next command.
				// No need to throttle.
				self.throttle.Store(false)
			case <-opts.Stop:
				// we use the time it took to start the program as a way of checking if things
				// are running slow at the moment. This is admittedly a crude estimate, but
				// the point is that we only want to throttle when things are running slow
				// and the user is flicking through a bunch of items.
				self.throttle.Store(time.Since(startTime) < THROTTLE_TIME && timeToStart > COMMAND_START_THRESHOLD)

				// Kill the still-running command. The only reason to do this is to save CPU usage
				// when flicking through several very long diffs when diff.algorithm = histogram is
				// being used, in which case multiple git processes continue to calculate expensive
				// diffs in the background even though they have been stopped already.
				if err := cmd.Terminate(); err != nil {
					self.Log.Errorf("error when trying to terminate cmd task: %v; Command: %v", err, cmd.String())
				}

				// close the task's stdout pipe (or the pty if we're using one) to make the command terminate
				onDone()
			}
		})

		loadingMutex := deadlock.Mutex{}

		readRequests := self.readRequests.beginServing(initialRequest)

		scanner := bufio.NewScanner(r)
		scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize))

		lineChan := make(chan []byte)
		lineWrittenChan := make(chan struct{})

		// We're reading from the scanner in a separate goroutine because on windows
		// if running git through a shim, we sometimes kill the parent process without
		// killing its children, meaning the scanner blocks forever. This solution
		// leaves us with a dead goroutine, but it's better than blocking all
		// rendering to main views.
		go utils.Safe(func() {
			defer close(lineChan)
			for scanner.Scan() {
				select {
				case <-opts.Stop:
					return
				case lineChan <- scanner.Bytes():
					// We need to confirm the data has been fed into the view before we
					// pull more from the scanner because the scanner uses the same backing
					// array and we don't want to be mutating that while it's being written
					<-lineWrittenChan
				}
			}

			if err := scanner.Err(); err != nil {
				self.Log.Error(err)
			}
		})

		loaded := false

		go utils.Safe(func() {
			ticker := time.NewTicker(time.Millisecond * 200)
			defer ticker.Stop()
			select {
			case <-opts.Stop:
				return
			case <-ticker.C:
				loadingMutex.Lock()
				// Only take the view over to say "loading..." when the content coming
				// is different from what's on screen. A re-render of the same content
				// leaves the view showing exactly what it should already, so clearing
				// it for the message and then rendering the same thing back is a
				// visible flicker for nothing — and a slow re-render of unchanged
				// content is common (a background refresh over a repo with submodules
				// that have uncommitted changes, say). The pending flag isn't consumed
				// here; the first paint still owes the scroll reset.
				//
				// A restore keeps the view too: it is there to make a re-render of what
				// the user is looking at seamless, and blanking the view for a message
				// before putting them back where they were is the flicker it exists to
				// avoid.
				if !loaded && restore == nil && self.newContentPending.Load() {
					self.beforeStart()
					// beforeStart cleared the previous content to show "loading...", so
					// put the view back at the top for it (beforeStart doesn't touch the
					// origin). The origin is view state the UI thread reads while laying
					// out, so write it there.
					_ = self.onUIThread(self.resetOrigin)
					_, _ = self.writer.Write([]byte("loading..."))
					self.refreshView()
				}
				loadingMutex.Unlock()
			}
		})

		go utils.Safe(func() {
			isViewStale := true
			writeToView := func(content []byte) {
				isViewStale = true
				_, _ = self.writer.Write(content)
			}
			refreshViewIfStale := func() {
				if isViewStale {
					self.refreshView()
					isViewStale = false
				}
			}

			// The total number of lines we have read so far. Requests specify an
			// absolute target total (see LinesToRead.Total), so we compare against
			// this to work out how many more lines, if any, we still need to read.
			linesRead := 0

			// The first paint swaps the off-screen render in to reveal the new
			// content, and settles the scroll position in the same step — so the new
			// content first appears already where it belongs, and no draw can land
			// between the two and show it at the previous render's scroll. It happens
			// once, either when we've read far enough (below) or at end of input for
			// content shorter than that. Callers run it on the UI thread: it writes
			// the view's origin.
			painted := false
			firstPaint := func() {
				if painted {
					return
				}
				painted = true
				// Content the view hasn't seen is shown from the top, and this is where
				// the view goes there — before the restore below, which decides where to
				// put the view from where it is. The position the paint settles on is the
				// restore's to move from, so it has to be the one the new content is
				// about to be revealed at.
				if self.newContentPending.Swap(false) {
					self.resetOrigin()
				}
				if restore != nil {
					// The restore does the swap itself, so that it can find where the
					// user was in the new content before it is revealed.
					restore.Apply(self.swapInRender)
					self.clearRestore(restore)
					restore.resolved()
					return
				}
				self.swapInRender()
			}

			// Set LAZYGIT_SLOW_RENDER=<milliseconds> to sleep that long after each
			// line is written to the view, stretching async loads out so the frames
			// of a re-render become visible. Useful for debugging scroll/flicker
			// behaviour; has no effect when the variable is unset.
			var slowRenderPerLine time.Duration
			if v := os.Getenv("LAZYGIT_SLOW_RENDER"); v != "" {
				if ms, err := strconv.Atoi(v); err == nil {
					slowRenderPerLine = time.Duration(ms) * time.Millisecond
				}
			}

			// Go's select picks randomly among ready cases, so once opts.Stop is
			// closed the selects below could still service a ready data channel
			// instead of bailing. Check stop explicitly first to give it priority:
			// a task that's been stopped (it's being replaced by a newer one) must
			// not touch the view here — it would start an off-screen render and
			// write the prefix into it, clobbering what the incoming task is about
			// to render.
		outer:
			for {
				if stopped() {
					break outer
				}
				linesToRead, ok := self.readRequests.dequeue()
				if !ok {
					// Nothing to read yet: wait to be told there is, or to be stopped.
					select {
					case <-opts.Stop:
						break outer
					case <-readRequests:
					}
					continue
				}
				{
					callThen := func() {
						if linesToRead.Then != nil {
							linesToRead.Then()
						}
					}
					// A task that is stopped is being replaced by one that renders the
					// content the caller is going to act on, so the request is left for
					// that one to serve. A request without a Then has nobody waiting on
					// it, and the task replacing this one reads what it needs by itself.
					handOver := func() {
						if linesToRead.Then != nil {
							self.readRequests.requeue(linesToRead)
						}
					}
					// A restore that hasn't painted yet keeps us reading past the lines
					// asked for, all the way to the end of the input if need be. What it
					// is looking for may be anywhere in the new content, and a rendering
					// that has to be parsed as a diff to be searched at all can only be
					// parsed whole — so stopping early would leave it nothing to find,
					// and the view somewhere the user didn't put it.
					for linesToRead.Total == -1 || linesRead < linesToRead.Total || (restore != nil && !painted) {
						if stopped() {
							handOver()
							break outer
						}
						var ok bool
						var line []byte
						select {
						case <-opts.Stop:
							handOver()
							break outer
						case line, ok = <-lineChan:
							// process line below
						}

						loadingMutex.Lock()
						if !loaded {
							// Build the new content off-screen, leaving the previous render
							// displayed until we swap in below; this is what keeps an async
							// re-render from showing a half-loaded buffer.
							self.beginRender()
							if prefixText != "" {
								writeToView([]byte(prefixText))
							}
							loaded = true
						}
						loadingMutex.Unlock()

						if !ok {
							// lineChan is closed. At a genuine end of input we swap in what we
							// read and finalize. But lineChan is also closed when this task has
							// been stopped to make way for a newer one: stopping closes
							// opts.Stop, and the scanner goroutine then closes lineChan, so the
							// select above can land here instead of on the opts.Stop case. A
							// stopped task is being replaced and must leave the view to the
							// incoming task — swapping in its half-read buffer, clamping the
							// origin, or clearing `loading` would all corrupt what that task is
							// about to render. So bail out here, the same as the explicit stop
							// case above.
							select {
							case <-opts.Stop:
								handOver()
								break outer
							default:
							}
							// Genuine end of input: do the first paint now if it hasn't happened
							// yet (the content was shorter than a screenful, so we never reached
							// the point below), and flush the stale content. onEndOfInput reads
							// the view's dimensions (to decide whether to scroll) and sets the
							// origin, both of which are UI-thread-only, so run it there — as is
							// firstPaint, which also writes the origin.
							_ = self.onUIThread(func() {
								firstPaint()
								self.onEndOfInput()
							})
							// The content is fully loaded now, so it's safe again for the
							// layout to clamp the scroll position to it.
							self.finishLoading(opts.taskID)
							callThen()
							break outer
						}
						writeToView(append(line, '\n'))
						lineWrittenChan <- struct{}{}
						linesRead++

						if slowRenderPerLine > 0 {
							time.Sleep(slowRenderPerLine)
						}

						if !painted {
							// Do the first paint once we have read enough lines to fill the
							// view — or, when a position is waiting to be restored, once the
							// restore says it can show it, since where the view should be is
							// its call. Continue reading afterwards and refresh again at the
							// end to make sure the scrollbar has the right size.
							var ready bool
							if restore != nil {
								ready = restore.FirstPaintReady()
							} else {
								ready = linesRead == linesToRead.InitialRefreshAfter
							}
							if ready {
								_ = self.onUIThread(firstPaint)
								refreshViewIfStale()
							}
						}
					}
					refreshViewIfStale()
					onFirstPageShown()
					callThen()
				}
			}

			// Whoever made a request the loop never got to is waiting to hear that the
			// content it asked for has been read. At end of input it has all been read
			// already. A task that was stopped leaves the requests to the one replacing
			// it.
			self.answerReadRequestsIfLatest(opts.taskID)

			refreshViewIfStale()

			select {
			case <-opts.Stop:
				// If we stopped the task, don't block waiting for it; this could cause a delay if
				// the process takes a while until it actually terminates. We still want to call
				// Wait to reclaim any resources, but do it on a background goroutine, and ignore
				// any errors.
				go utils.Safe(func() { _ = cmd.Wait() })
			default:
				if err := cmd.Wait(); err != nil {
					self.Log.Errorf("Unexpected error when running cmd task: %v; Failed command: %v", err, cmd.String())
				}
			}

			// calling this here again in case the program ended on its own accord
			onDone()

			close(done)
			close(lineWrittenChan)
		})

		<-done

		return nil
	}
}

// Close closes the task manager, killing whatever task may currently be running
func (self *ViewBufferManager) Close() {
	// No task is going to read anything after this, so nobody may be left waiting
	// for content to be read.
	defer self.stopServingReadRequests()

	// stopCurrentTask is written by NewTask's goroutine under waitingMutex (and
	// so is the sync.Once it closes over), so read it under the lock and call
	// the captured value; a task starting on shutdown must not race us here.
	self.waitingMutex.Lock()
	stopCurrentTask := self.stopCurrentTask
	self.waitingMutex.Unlock()

	if stopCurrentTask == nil {
		return
	}

	c := make(chan struct{})

	go utils.Safe(func() {
		stopCurrentTask()
		c <- struct{}{}
	})

	select {
	case <-c:
		return
	case <-time.After(3 * time.Second):
		fmt.Println("cannot kill child process")
	}
}

// different kinds of tasks:
// 1) command based, where the manager can be asked to read more lines,  but the command can be killed
// 2) string based, where the manager can also be asked to read more lines

type TaskOpts struct {
	// Channel that tells the task to stop, because another task wants to run.
	Stop chan struct{}

	// Only for tasks which are long-running, where we read more lines sporadically.
	// We use this to keep track of when a user's action is complete (i.e. all views
	// have been refreshed to display the results of their action)
	InitialContentLoaded func()

	// The task's place in the order of the view's tasks (see ReserveTask).
	taskID int
}

// A TaskReservation holds a task's place in the order of the view's tasks, from
// when the task is asked for until it is created. See ReserveTask.
type TaskReservation struct {
	taskID int
}

// ReserveTask gives a task its place in the order of the view's tasks, for a task
// that is only created later, with NewReservedTask. The view shows the task that
// was asked for last. If another task is asked for after the reservation, that
// task replaces the reserved one, even when it is created first.
func (self *ViewBufferManager) ReserveTask() TaskReservation {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	self.newTaskID++
	// Whoever asks for content to be read from now on is asking for this task's.
	self.readRequests.hold()
	return TaskReservation{taskID: self.newTaskID}
}

// IsSuperseded reports whether another task has been asked for since the
// reservation was made. The reserved task would then stop as soon as it was
// created, so there is no point in creating it.
func (self *ViewBufferManager) IsSuperseded(reservation TaskReservation) bool {
	self.taskIDMutex.Lock()
	defer self.taskIDMutex.Unlock()

	return reservation.taskID < self.newTaskID
}

// NewTask creates a task that takes its place in the order of the view's tasks
// right away.
func (self *ViewBufferManager) NewTask(f func(TaskOpts) error, key string) error {
	return self.NewReservedTask(self.ReserveTask(), f, key)
}

// NewReservedTask creates the task that the reservation was made for. It doesn't
// run if another task has been asked for since the reservation was made.
func (self *ViewBufferManager) NewReservedTask(reservation TaskReservation, f func(TaskOpts) error, key string) error {
	gocuiTask := self.newGocuiTask()

	var completeTaskOnce sync.Once

	completeGocuiTask := func() {
		completeTaskOnce.Do(func() {
			gocuiTask.Done()
		})
	}

	taskID := reservation.taskID

	go utils.Safe(func() {
		defer completeGocuiTask()

		self.taskIDMutex.Lock()

		// Bail out before touching shared view state if a newer task has
		// already been queued: if we reset the view here we'd do it for a task
		// that's about to exit, potentially wiping output the winning task has
		// already written.
		if taskID < self.newTaskID {
			self.taskIDMutex.Unlock()
			return
		}

		// Note we don't reset the origin here even when the command key changed:
		// that's deferred to the first paint that reveals the new content (see
		// newContentPending), so the previous content — left displayed until the
		// swap — doesn't visibly jump to the top before the new content appears.
		// Read taskKey directly: we already hold the mutex that guards it, and
		// GetTaskKey would take it again. A pending restore isn't dropped here
		// either, even for a different command: the re-renders it rides are all
		// different commands (a different context size, another diff renderer), and
		// it validates itself against the content it lands in anyway.
		// A task told to keep the scroll position renders the content the view is
		// already showing, laid out differently, so the reset it would otherwise owe
		// would take the user away from what they are reading — and the loading
		// message, which the same flag governs, would blank content that is about to
		// come back looking much the same.
		if self.taskKey != key && self.resetOrigin != nil && !self.keepScrollForNextTask {
			self.newContentPending.Store(true)
		}
		self.keepScrollForNextTask = false
		self.taskKey = key

		self.taskIDMutex.Unlock()

		self.waitingMutex.Lock()

		// Re-check staleness after acquiring waitingMutex: a newer task
		// may have arrived while we were blocked here.
		self.taskIDMutex.Lock()
		if taskID < self.newTaskID {
			self.waitingMutex.Unlock()
			self.taskIDMutex.Unlock()
			return
		}
		self.taskIDMutex.Unlock()

		if self.stopCurrentTask != nil {
			self.stopCurrentTask()
		}

		stop := make(chan struct{})
		notifyStopped := make(chan struct{})

		var once sync.Once
		onStop := func() {
			close(stop)
			<-notifyStopped
		}

		self.stopCurrentTask = func() { once.Do(onStop) }

		self.waitingMutex.Unlock()

		if err := f(TaskOpts{Stop: stop, InitialContentLoaded: completeGocuiTask, taskID: taskID}); err != nil {
			self.Log.Error(err) // might need an onError callback
		}

		// A task that reads no command's output, such as one showing a message, has
		// given the view all of its content once it returns.
		self.answerReadRequestsIfLatest(taskID)

		close(notifyStopped)
	})

	return nil
}