Bump gocui

This includes new gocui logic for tracking busy/idle program state
This commit is contained in:
Jesse Duffield 2023-07-08 22:25:58 +10:00
parent 585ea361f6
commit 631cf1e873
29 changed files with 273 additions and 59 deletions

View file

@ -173,6 +173,13 @@ type Gui struct {
screen tcell.Screen
suspendedMutex sync.Mutex
suspended bool
// Tracks whether the program is busy (i.e. either something is happening on
// the main goroutine or a worker goroutine). Used by integration tests
// to wait until the program is idle before progressing.
busyCount int
busyCountMutex sync.Mutex
idleListeners []chan struct{}
}
// NewGui returns a new Gui object with a given output mode.
@ -230,6 +237,13 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless
return g, nil
}
// An idle listener listens for when the program is idle. This is useful for
// integration tests which can wait for the program to be idle before taking
// the next step in the test.
func (g *Gui) AddIdleListener(c chan struct{}) {
g.idleListeners = append(g.idleListeners, c)
}
// Close finalizes the library. It should be called after a successful
// initialization and when gocui is not needed anymore.
func (g *Gui) Close() {
@ -602,16 +616,51 @@ type userEvent struct {
// the user events queue. Given that Update spawns a goroutine, the order in
// which the user events will be handled is not guaranteed.
func (g *Gui) Update(f func(*Gui) error) {
go g.UpdateAsync(f)
g.IncrementBusyCount()
go g.updateAsyncAux(f)
}
// UpdateAsync is a version of Update that does not spawn a go routine, it can
// be a bit more efficient in cases where Update is called many times like when
// tailing a file. In general you should use Update()
func (g *Gui) UpdateAsync(f func(*Gui) error) {
g.IncrementBusyCount()
g.updateAsyncAux(f)
}
func (g *Gui) updateAsyncAux(f func(*Gui) error) {
g.userEvents <- userEvent{f: f}
}
// Calls a function in a goroutine. Handles panics gracefully and tracks
// number of background tasks.
// Always use this when you want to spawn a goroutine and you want lazygit to
// consider itself 'busy` as it runs the code. Don't use for long-running
// background goroutines where you wouldn't want lazygit to be considered busy
// (i.e. when you wouldn't want a loader to be shown to the user)
func (g *Gui) OnWorker(f func()) {
g.IncrementBusyCount()
go func() {
g.onWorkerAux(f)
g.DecrementBusyCount()
}()
}
func (g *Gui) onWorkerAux(f func()) {
panicking := true
defer func() {
if panicking && Screen != nil {
Screen.Fini()
}
}()
f()
panicking = false
}
// A Manager is in charge of GUI's layout and can be used to build widgets.
type Manager interface {
// Layout is called every time the GUI is redrawn, it must contain the
@ -666,27 +715,68 @@ func (g *Gui) MainLoop() error {
}
for {
select {
case ev := <-g.gEvents:
if err := g.handleEvent(&ev); err != nil {
return err
}
case ev := <-g.userEvents:
if err := ev.f(g); err != nil {
return err
}
}
if err := g.consumeevents(); err != nil {
return err
}
if err := g.flush(); err != nil {
err := g.processEvent()
if err != nil {
return err
}
}
}
// consumeevents handles the remaining events in the events pool.
func (g *Gui) consumeevents() error {
func (g *Gui) IncrementBusyCount() {
g.busyCountMutex.Lock()
defer g.busyCountMutex.Unlock()
g.busyCount++
}
func (g *Gui) DecrementBusyCount() {
g.busyCountMutex.Lock()
defer g.busyCountMutex.Unlock()
if g.busyCount == 0 {
panic("busyCount is already 0")
}
if g.busyCount == 1 {
// notify listeners that the program is idle
for _, listener := range g.idleListeners {
listener <- struct{}{}
}
}
g.busyCount--
}
func (g *Gui) processEvent() error {
select {
case ev := <-g.gEvents:
g.IncrementBusyCount()
defer func() { g.DecrementBusyCount() }()
if err := g.handleEvent(&ev); err != nil {
return err
}
case ev := <-g.userEvents:
// user events increment busyCount ahead of time
defer func() { g.DecrementBusyCount() }()
if err := ev.f(g); err != nil {
return err
}
}
if err := g.processRemainingEvents(); err != nil {
return err
}
if err := g.flush(); err != nil {
return err
}
return nil
}
// processRemainingEvents handles the remaining events in the events pool.
func (g *Gui) processRemainingEvents() error {
for {
select {
case ev := <-g.gEvents:
@ -694,7 +784,9 @@ func (g *Gui) consumeevents() error {
return err
}
case ev := <-g.userEvents:
if err := ev.f(g); err != nil {
err := ev.f(g)
g.DecrementBusyCount()
if err != nil {
return err
}
default:
@ -1355,7 +1447,7 @@ func (g *Gui) StartTicking(ctx context.Context) {
for _, view := range g.Views() {
if view.HasLoader {
g.userEvents <- userEvent{func(g *Gui) error { return nil }}
g.UpdateAsync(func(g *Gui) error { return nil })
continue outer
}
}