From 7a9e4bee23517c11fb54d39f7c7b2ef113919a69 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Oct 2026 09:51:23 +0200 Subject: [PATCH] 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) --- pkg/commands/git_commands/branch_loader.go | 8 ++++---- pkg/commands/git_commands/github.go | 5 +++-- pkg/gui/controllers/helpers/drag_autoscroller.go | 5 +++-- pkg/gui/controllers/helpers/fixup_helper.go | 8 ++++---- pkg/gui/controllers/main_view_controller.go | 5 +++-- pkg/utils/utils.go | 12 ++++++++++++ 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go index 6091a4679..3053cd6fa 100644 --- a/pkg/commands/git_commands/branch_loader.go +++ b/pkg/commands/git_commands/branch_loader.go @@ -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() diff --git a/pkg/commands/git_commands/github.go b/pkg/commands/git_commands/github.go index 2b0568685..0d0fc2ee8 100644 --- a/pkg/commands/git_commands/github.go +++ b/pkg/commands/git_commands/github.go @@ -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 diff --git a/pkg/gui/controllers/helpers/drag_autoscroller.go b/pkg/gui/controllers/helpers/drag_autoscroller.go index 7ec6a9c76..c9a468911 100644 --- a/pkg/gui/controllers/helpers/drag_autoscroller.go +++ b/pkg/gui/controllers/helpers/drag_autoscroller.go @@ -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) { diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go index bac22a209..2e3250a3d 100644 --- a/pkg/gui/controllers/helpers/fixup_helper.go +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -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() { diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 487b15549..1ff6c52bb 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -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 } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index d706fe719..d690fc82a 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -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)