mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
Blocking keyboard input and hiding the working tree state mode are two separate concerns; they were fused into one helper because every caller so far wanted both. A caller that blocks input for something other than a rebase would then hide the "Rebasing" indicator for the duration of its operation, which has nothing to do with it. Make it an explicit option instead, so blocking input on its own doesn't imply anything about the modes on display.
173 lines
6.6 KiB
Go
173 lines
6.6 KiB
Go
package helpers
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/status"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
)
|
|
|
|
type AppStatusHelper struct {
|
|
c *HelperCommon
|
|
|
|
statusMgr func() *status.StatusManager
|
|
modeHelper *ModeHelper
|
|
}
|
|
|
|
func NewAppStatusHelper(c *HelperCommon, statusMgr func() *status.StatusManager, modeHelper *ModeHelper) *AppStatusHelper {
|
|
return &AppStatusHelper{
|
|
c: c,
|
|
statusMgr: statusMgr,
|
|
modeHelper: modeHelper,
|
|
}
|
|
}
|
|
|
|
func (self *AppStatusHelper) Toast(message string, kind types.ToastKind) {
|
|
if self.c.RunningIntegrationTest() {
|
|
// Don't bother showing toasts in integration tests. You can't check for
|
|
// them anyway, and they would only slow down the test unnecessarily by
|
|
// two seconds.
|
|
return
|
|
}
|
|
|
|
self.statusMgr().AddToastStatus(message, kind)
|
|
|
|
self.renderAppStatus()
|
|
}
|
|
|
|
// A custom task for WithWaitingStatus calls; it wraps the original one and
|
|
// hides the status whenever the task is paused, and shows it again when
|
|
// continued.
|
|
type appStatusHelperTask struct {
|
|
gocui.Task
|
|
waitingStatusHandle *status.WaitingStatusHandle
|
|
}
|
|
|
|
// poor man's version of explicitly saying that struct X implements interface Y
|
|
var _ gocui.Task = appStatusHelperTask{}
|
|
|
|
func (self appStatusHelperTask) Pause() {
|
|
self.waitingStatusHandle.Hide()
|
|
self.Task.Pause()
|
|
}
|
|
|
|
func (self appStatusHelperTask) Continue() {
|
|
self.Task.Continue()
|
|
self.waitingStatusHandle.Show()
|
|
}
|
|
|
|
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
|
|
func (self *AppStatusHelper) WithWaitingStatus(message string, f func(gocui.Task) error) {
|
|
self.c.OnWorker(func(task gocui.Task) error {
|
|
return self.WithWaitingStatusImpl(message, f, task)
|
|
})
|
|
}
|
|
|
|
// WithWaitingStatusImpl is WithWaitingStatus for callers that already run on a
|
|
// goroutine of their own (e.g. the auto-fetch poller) rather than wanting the
|
|
// work dispatched to a worker. task is used to hide the status while the task
|
|
// is paused; it may be nil for callers whose f ignores its task.
|
|
func (self *AppStatusHelper) WithWaitingStatusImpl(message string, f func(gocui.Task) error, task gocui.Task) error {
|
|
// A waiting status means lazygit is driving a git operation itself (often
|
|
// one that internally runs a rebase and continues it). Pause the background
|
|
// routines for its duration so they don't refresh from an intermediate
|
|
// state and reveal, say, the half-finished history of a reword.
|
|
self.c.PauseBackgroundRefreshes(true)
|
|
defer self.c.PauseBackgroundRefreshes(false)
|
|
|
|
return self.statusMgr().WithWaitingStatus(message, self.renderAppStatus, func(waitingStatusHandle *status.WaitingStatusHandle) error {
|
|
return f(appStatusHelperTask{task, waitingStatusHandle})
|
|
})
|
|
}
|
|
|
|
// WithWaitingStatusBlockingInput is like WithWaitingStatus, but it also blocks
|
|
// keyboard input for the whole duration of the operation: keys the user presses
|
|
// while it runs are buffered and replayed against the post-operation state (see
|
|
// gocui.BeginBlockingEvents). Use it for operations whose following keypress
|
|
// depends on the state they produce, e.g. ones that manipulate an in-progress
|
|
// rebase or otherwise rewrite commits, where a racing keypress would target the
|
|
// wrong commit or todo.
|
|
//
|
|
// Must be called on the UI thread: the block is begun synchronously here, before
|
|
// the operation is dispatched to a worker, so no keypress can slip through in
|
|
// between.
|
|
func (self *AppStatusHelper) WithWaitingStatusBlockingInput(opts types.WaitingStatusOpts, f func(gocui.Task) error) {
|
|
self.c.GocuiGui().BeginBlockingEvents()
|
|
if opts.HideWorkingTreeState {
|
|
self.modeHelper.SetSuppressWorkingTreeStateMode(true)
|
|
}
|
|
self.c.OnWorker(func(task gocui.Task) error {
|
|
// End the block and restore the mode indicator once the operation and its
|
|
// refresh have applied their UI updates: OnUIThread queues this after the
|
|
// refresh's model bounces and Then (which RefreshFromWorker has already
|
|
// enqueued by the time f returns), so the replayed keys act on the
|
|
// refreshed state and any resulting working tree state shows correctly.
|
|
defer self.c.OnUIThread(func() error {
|
|
if opts.HideWorkingTreeState {
|
|
self.modeHelper.SetSuppressWorkingTreeStateMode(false)
|
|
}
|
|
return self.c.GocuiGui().EndBlockingEvents()
|
|
})
|
|
return self.WithWaitingStatusImpl(opts.Message, f, task)
|
|
})
|
|
}
|
|
|
|
func (self *AppStatusHelper) HasStatus() bool {
|
|
return self.statusMgr().HasStatus()
|
|
}
|
|
|
|
func (self *AppStatusHelper) GetStatusString() string {
|
|
appStatus, _ := self.statusMgr().GetStatusString(self.c.UserConfig())
|
|
return appStatus
|
|
}
|
|
|
|
// renderAppStatus ensures the render loop that keeps the app-status view up to
|
|
// date is running. There is one loop for the whole status stack, no matter how
|
|
// many statuses are showing: it draws whatever the top status currently is,
|
|
// and exits after drawing a final empty frame once the last status is removed.
|
|
//
|
|
// The loop always runs as a background task, regardless of what kind of
|
|
// operation owns a status: rendering runs no git commands, so it must never
|
|
// count towards lazygit being busy — otherwise it would block repo switching
|
|
// for as long as anything is showing (e.g. for the whole duration of a hung
|
|
// background fetch, or of a toast fading). A foreground operation's busy-ness
|
|
// is carried by its own worker task, not by the renderer.
|
|
func (self *AppStatusHelper) renderAppStatus() {
|
|
if !self.statusMgr().ClaimRenderLoop() {
|
|
return
|
|
}
|
|
|
|
self.c.OnWorkerBackground(func(_ gocui.Task) error {
|
|
ticker := time.NewTicker(time.Millisecond * time.Duration(self.c.UserConfig().Gui.Spinner.Rate))
|
|
defer ticker.Stop()
|
|
prevAppStatus := ""
|
|
for range ticker.C {
|
|
appStatus, color := self.statusMgr().GetStatusString(self.c.UserConfig())
|
|
|
|
update := self.c.OnUIThreadContentOnlyBackground
|
|
if utils.StringWidth(appStatus) != utils.StringWidth(prevAppStatus) {
|
|
// Need a full layout whenever the width of the status string changes. This can't
|
|
// happen during normal spinning because we validate that all spinner frames have
|
|
// the same width, so typically this will only be triggered at the beginning and end
|
|
// of a status, or if the status string changes midway for some reason.
|
|
update = self.c.OnUIThreadBackground
|
|
}
|
|
update(func() error {
|
|
self.c.Views().AppStatus.FgColor = color
|
|
self.c.SetViewContent(self.c.Views().AppStatus, appStatus)
|
|
return nil
|
|
})
|
|
prevAppStatus = appStatus
|
|
|
|
// Checked after rendering, so that the frame which clears the view
|
|
// has already been drawn when we exit.
|
|
if self.statusMgr().ReleaseRenderLoopIfEmpty() {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|