Restore the terminal if a timer or errgroup function panics

time.AfterFunc and errgroup.Group.Go run the function they are given
on a goroutine of their own, so a panic in it leaves the terminal in
raw mode, like a panic in any other goroutine that doesn't use
utils.Safe.

Add utils.SafeFunc and utils.SafeFuncWithError, which return a copy
of a function that runs through utils.Safe or utils.SafeWithError, and
use them for the functions given to time.AfterFunc and errgroup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-10-14 20:28:53 +02:00
co-authored by Claude Opus 5.5
parent 73fac4a554
commit 7a9e4bee23
6 changed files with 29 additions and 14 deletions
+4 -4
View File
@@ -177,7 +177,7 @@ func (self *BranchLoader) checkForRewrittenUpstreams(branches []*models.Branch,
continue
}
errg.Go(func() error {
errg.Go(utils.SafeFuncWithError(func() error {
hasLocalOnlyCommits, err := self.hasLocalOnlyCommits(branch)
if err != nil {
// Not worth bothering the user about; it only means that we
@@ -186,7 +186,7 @@ func (self *BranchLoader) checkForRewrittenUpstreams(branches []*models.Branch,
}
branch.UpstreamRewritten.Store(err == nil && !hasLocalOnlyCommits)
return nil
})
}))
}
err := errg.Wait()
@@ -220,7 +220,7 @@ func (self *BranchLoader) getBehindBaseBranchValuesLegacy(
errg := errgroup.Group{}
for _, branch := range branches {
errg.Go(func() error {
errg.Go(utils.SafeFuncWithError(func() error {
baseBranch, err := self.GetBaseBranch(branch, mainBranches)
if err != nil {
return err
@@ -247,7 +247,7 @@ func (self *BranchLoader) getBehindBaseBranchValuesLegacy(
}
branch.BehindBaseBranch.Store(int32(behind))
return nil
})
}))
}
err := errg.Wait()
+3 -2
View File
@@ -15,6 +15,7 @@ import (
"github.com/cli/go-gh/v2/pkg/auth"
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
)
@@ -233,14 +234,14 @@ func (self *GitHubCommands) FetchRecentPRs(branches []string, serviceInfo *hosti
branchChunk := branches[i:end]
// Launch a goroutine for each chunk of branches
g.Go(func() error {
g.Go(utils.SafeFuncWithError(func() error {
prs, err := self.fetchRecentPRsAux(endpoint, serviceInfo.Owner, serviceInfo.Repository, branchChunk, token)
if err != nil {
return err
}
results <- prs
return nil
})
}))
}
// Wait for all goroutines, then close the channel so the range loop exits
@@ -4,6 +4,7 @@ import (
"time"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
const (
@@ -91,12 +92,12 @@ func (self *DragAutoscroller) Cancel() {
}
func (self *DragAutoscroller) schedule(generation uint64, delay time.Duration) {
time.AfterFunc(delay, func() {
time.AfterFunc(delay, utils.SafeFunc(func() {
self.c.OnUIThreadBackground(func() error {
self.tick(generation)
return nil
})
})
}))
}
func (self *DragAutoscroller) tick(generation uint64) {
+4 -4
View File
@@ -267,7 +267,7 @@ func (self *FixupHelper) blameDeletedLines(deletedLineHunks []*hunk) ([]string,
hashChan := make(chan string)
for _, h := range deletedLineHunks {
errg.Go(func() error {
errg.Go(utils.SafeFuncWithError(func() error {
blameOutput, err := self.c.Git().Blame.BlameLineRange(h.filename, "HEAD", h.startLineIdx, h.numLines)
if err != nil {
return err
@@ -277,7 +277,7 @@ func (self *FixupHelper) blameDeletedLines(deletedLineHunks []*hunk) ([]string,
hashChan <- strings.Split(line, " ")[0]
}
return nil
})
}))
}
go utils.Safe(func() {
@@ -301,7 +301,7 @@ func (self *FixupHelper) blameAddedLines(commits []*models.Commit, addedLineHunk
hashesChan := make(chan []string)
for _, h := range addedLineHunks {
errg.Go(func() error {
errg.Go(utils.SafeFuncWithError(func() error {
result := make([]string, 0, 2)
appendBlamedLine := func(blameOutput string) {
@@ -340,7 +340,7 @@ func (self *FixupHelper) blameAddedLines(commits []*models.Commit, addedLineHunk
hashesChan <- result
return nil
})
}))
}
go utils.Safe(func() {
+3 -2
View File
@@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@@ -629,14 +630,14 @@ func (self *MainViewController) editClickedLine(opts gocui.ViewMouseBindingOpts)
self.c.GocuiGui().ForceFlushViewsContentOnly(self.c.GocuiGui().Views())
})
if flashGeneration != 0 {
time.AfterFunc(editedLineFlashDuration, func() {
time.AfterFunc(editedLineFlashDuration, utils.SafeFunc(func() {
self.c.OnUIThreadContentOnlyBackground(func() error {
if self.lineFlashGeneration == flashGeneration {
self.context.GetView().ClearLineFlash()
}
return nil
})
})
}))
}
return err
}
+12
View File
@@ -85,6 +85,18 @@ func SafeWithError(f func() error) error {
return err
}
// SafeFunc returns a function that runs f through Safe. Use it for functions
// that something else runs on a goroutine of its own, such as time.AfterFunc.
func SafeFunc(f func()) func() {
return func() { Safe(f) }
}
// SafeFuncWithError is like SafeFunc, for functions that return an error, such
// as the ones passed to errgroup.Group.Go.
func SafeFuncWithError(f func() error) func() error {
return func() error { return SafeWithError(f) }
}
func StackTrace() string {
buf := make([]byte, 10000)
n := runtime.Stack(buf, false)