From 7075b66bc6dde80be5e0bc626db1988a9f47e2ca Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 22 Sep 2023 16:09:02 +0200 Subject: [PATCH] Add WithInlineStatus helper function Very similar to WithWaitingStatus, except that the status is shown in a view next to the affected item, rather than in the status bar. Not used by anything yet; again, committing separately to get smaller commits. --- pkg/gui/controllers.go | 1 + pkg/gui/controllers/helpers/helpers.go | 2 + .../helpers/inline_status_helper.go | 129 ++++++++++++++++++ pkg/gui/gui_common.go | 6 + pkg/gui/types/common.go | 6 + 5 files changed, 144 insertions(+) create mode 100644 pkg/gui/controllers/helpers/inline_status_helper.go diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 40a482f30..43b226b58 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -112,6 +112,7 @@ func (gui *Gui) resetHelpersAndControllers() { Confirmation: helpers.NewConfirmationHelper(helperCommon), Mode: modeHelper, AppStatus: appStatusHelper, + InlineStatus: helpers.NewInlineStatusHelper(helperCommon), WindowArrangement: helpers.NewWindowArrangementHelper( gui.c, windowHelper, diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 06c5cc973..9e05ba163 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -46,6 +46,7 @@ type Helpers struct { Confirmation *ConfirmationHelper Mode *ModeHelper AppStatus *AppStatusHelper + InlineStatus *InlineStatusHelper WindowArrangement *WindowArrangementHelper Search *SearchHelper Worktree *WorktreeHelper @@ -81,6 +82,7 @@ func NewStubHelpers() *Helpers { Confirmation: &ConfirmationHelper{}, Mode: &ModeHelper{}, AppStatus: &AppStatusHelper{}, + InlineStatus: &InlineStatusHelper{}, WindowArrangement: &WindowArrangementHelper{}, Search: &SearchHelper{}, Worktree: &WorktreeHelper{}, diff --git a/pkg/gui/controllers/helpers/inline_status_helper.go b/pkg/gui/controllers/helpers/inline_status_helper.go new file mode 100644 index 000000000..d4435e5b9 --- /dev/null +++ b/pkg/gui/controllers/helpers/inline_status_helper.go @@ -0,0 +1,129 @@ +package helpers + +import ( + "time" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/sasha-s/go-deadlock" +) + +type InlineStatusHelper struct { + c *HelperCommon + + contextsWithInlineStatus map[types.ContextKey]*inlineStatusInfo + mutex *deadlock.Mutex +} + +func NewInlineStatusHelper(c *HelperCommon) *InlineStatusHelper { + return &InlineStatusHelper{ + c: c, + contextsWithInlineStatus: make(map[types.ContextKey]*inlineStatusInfo), + mutex: &deadlock.Mutex{}, + } +} + +type InlineStatusOpts struct { + Item types.HasUrn + Operation types.ItemOperation + ContextKey types.ContextKey +} + +type inlineStatusInfo struct { + refCount int + stop chan struct{} +} + +// A custom task for WithInlineStatus calls; it wraps the original one and +// hides the status whenever the task is paused, and shows it again when +// continued. +type inlineStatusHelperTask struct { + gocui.Task + + inlineStatusHelper *InlineStatusHelper + opts InlineStatusOpts +} + +// poor man's version of explicitly saying that struct X implements interface Y +var _ gocui.Task = inlineStatusHelperTask{} + +func (self inlineStatusHelperTask) Pause() { + self.inlineStatusHelper.stop(self.opts) + self.Task.Pause() + + self.inlineStatusHelper.renderContext(self.opts.ContextKey) +} + +func (self inlineStatusHelperTask) Continue() { + self.Task.Continue() + self.inlineStatusHelper.start(self.opts) +} + +func (self *InlineStatusHelper) WithInlineStatus(opts InlineStatusOpts, f func(gocui.Task) error) { + self.c.OnWorker(func(task gocui.Task) { + self.start(opts) + + err := f(inlineStatusHelperTask{task, self, opts}) + if err != nil { + self.c.OnUIThread(func() error { + return self.c.Error(err) + }) + } + + self.stop(opts) + }) +} + +func (self *InlineStatusHelper) start(opts InlineStatusOpts) { + self.c.State().SetItemOperation(opts.Item, opts.Operation) + + self.mutex.Lock() + defer self.mutex.Unlock() + + info := self.contextsWithInlineStatus[opts.ContextKey] + if info == nil { + info = &inlineStatusInfo{refCount: 0, stop: make(chan struct{})} + self.contextsWithInlineStatus[opts.ContextKey] = info + + go utils.Safe(func() { + ticker := time.NewTicker(time.Millisecond * utils.LoaderAnimationInterval) + defer ticker.Stop() + outer: + for { + select { + case <-ticker.C: + self.renderContext(opts.ContextKey) + case <-info.stop: + break outer + } + } + }) + } + + info.refCount++ +} + +func (self *InlineStatusHelper) stop(opts InlineStatusOpts) { + self.mutex.Lock() + + if info := self.contextsWithInlineStatus[opts.ContextKey]; info != nil { + info.refCount-- + if info.refCount <= 0 { + info.stop <- struct{}{} + delete(self.contextsWithInlineStatus, opts.ContextKey) + } + + } + + self.mutex.Unlock() + + self.c.State().ClearItemOperation(opts.Item) +} + +func (self *InlineStatusHelper) renderContext(contextKey types.ContextKey) { + self.c.OnUIThread(func() error { + _ = self.c.ContextForKey(contextKey).HandleRender() + return nil + }) +} diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index 0abe94f4c..4b9d3dc37 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -5,6 +5,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -199,3 +200,8 @@ func (self *guiCommon) RunningIntegrationTest() bool { func (self *guiCommon) InDemo() bool { return self.gui.integrationTest != nil && self.gui.integrationTest.IsDemo() } + +func (self *guiCommon) WithInlineStatus(item types.HasUrn, operation types.ItemOperation, contextKey types.ContextKey, f func(gocui.Task) error) error { + self.gui.helpers.InlineStatus.WithInlineStatus(helpers.InlineStatusOpts{Item: item, Operation: operation, ContextKey: contextKey}, f) + return nil +} diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index f4fde41c7..e9f9fc266 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -87,6 +87,12 @@ type IGuiCommon interface { // resized, if in accordion mode. AfterLayout(f func() error) + // Wraps a function, attaching the given operation to the given item while + // the function is executing, and also causes the given context to be + // redrawn periodically. This allows the operation to be visualized with a + // spinning loader animation (e.g. when a branch is being pushed). + WithInlineStatus(item HasUrn, operation ItemOperation, contextKey ContextKey, f func(gocui.Task) error) error + // returns the gocui Gui struct. There is a good chance you don't actually want to use // this struct and instead want to use another method above GocuiGui() *gocui.Gui