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 }) }