1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-03 00:57:52 +02:00
Files
lazygit/pkg/gui/controllers/workspace_reset_controller.go
Stefan Haller d82852a909 Change Refresh to not return an error
Refresh is one of those functions that shouldn't require error handling (similar
to triggering a redraw of the UI, see
https://github.com/jesseduffield/lazygit/issues/3887).

As far as I see, the only reason why Refresh can currently return an error is
that the Then function returns one. The actual refresh errors, e.g. from the git
calls that are made to fetch data, are already logged and swallowed. Most of the
Then functions do only UI stuff such as selecting a list item, and always return
nil; there's only one that can return an error (updating the rebase todo file in
LocalCommitsController.startInteractiveRebaseWithEdit); it's not a critical
error if this fails, it is only used for setting rebase todo items to "edit"
when you start an interactive rebase by pressing 'e' on a range selection of
commits. We simply log this error instead of returning it.
2025-07-02 16:09:42 +02:00

267 lines
6.8 KiB
Go

package controllers
import (
"bytes"
"errors"
"fmt"
"math"
"math/rand"
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// this is in its own file given that the workspace controller file is already quite long
func (self *FilesController) createResetMenu() error {
red := style.FgRed
nukeStr := "git reset --hard HEAD && git clean -fd"
if len(self.c.Model().Submodules) > 0 {
nukeStr = fmt.Sprintf("%s (%s)", nukeStr, self.c.Tr.AndResetSubmodules)
}
menuItems := []*types.MenuItem{
{
LabelColumns: []string{
self.c.Tr.DiscardAllChangesToAllFiles,
red.Sprint(nukeStr),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.NukeWorkingTree)
if err := self.c.Git().WorkingTree.ResetAndClean(); err != nil {
return err
}
if self.c.UserConfig().Gui.AnimateExplosion {
self.animateExplosion()
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'x',
Tooltip: self.c.Tr.NukeDescription,
},
{
LabelColumns: []string{
self.c.Tr.DiscardAnyUnstagedChanges,
red.Sprint("git checkout -- ."),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.DiscardUnstagedFileChanges)
if err := self.c.Git().WorkingTree.DiscardAnyUnstagedFileChanges(); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'u',
},
{
LabelColumns: []string{
self.c.Tr.DiscardUntrackedFiles,
red.Sprint("git clean -fd"),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RemoveUntrackedFiles)
if err := self.c.Git().WorkingTree.RemoveUntrackedFiles(); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'c',
},
{
LabelColumns: []string{
self.c.Tr.DiscardStagedChanges,
red.Sprint("stash staged and drop stash"),
},
Tooltip: self.c.Tr.DiscardStagedChangesDescription,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RemoveStagedFiles)
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return errors.New(self.c.Tr.NoTrackedStagedFilesStash)
}
if err := self.c.Git().Stash.SaveStagedChanges("[lazygit] tmp stash"); err != nil {
return err
}
if err := self.c.Git().Stash.DropNewest(); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'S',
},
{
LabelColumns: []string{
self.c.Tr.SoftReset,
red.Sprint("git reset --soft HEAD"),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.SoftReset)
if err := self.c.Git().WorkingTree.ResetSoft("HEAD"); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 's',
},
{
LabelColumns: []string{
"mixed reset",
red.Sprint("git reset --mixed HEAD"),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.MixedReset)
if err := self.c.Git().WorkingTree.ResetMixed("HEAD"); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'm',
},
{
LabelColumns: []string{
self.c.Tr.HardReset,
red.Sprint("git reset --hard HEAD"),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.HardReset)
if err := self.c.Git().WorkingTree.ResetHard("HEAD"); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'h',
},
}
return self.c.Menu(types.CreateMenuOptions{Title: "", Items: menuItems})
}
func (self *FilesController) animateExplosion() {
self.Explode(self.c.Views().Files, func() {
self.c.PostRefreshUpdate(self.c.Contexts().Files)
})
}
// Animates an explosion within the view by drawing a bunch of flamey characters
func (self *FilesController) Explode(v *gocui.View, onDone func()) {
width := v.InnerWidth()
height := v.InnerHeight()
styles := []style.TextStyle{
style.FgLightWhite.SetBold(),
style.FgYellow.SetBold(),
style.FgRed.SetBold(),
style.FgBlue.SetBold(),
style.FgBlack.SetBold(),
}
self.c.OnWorker(func(_ gocui.Task) error {
max := 25
for i := range max {
image := getExplodeImage(width, height, i, max)
style := styles[(i*len(styles)/max)%len(styles)]
coloredImage := style.Sprint(image)
self.c.OnUIThread(func() error {
v.SetOrigin(0, 0)
v.SetContent(coloredImage)
return nil
})
time.Sleep(time.Millisecond * 20)
}
self.c.OnUIThread(func() error {
v.Clear()
onDone()
return nil
})
return nil
})
}
// Render an explosion in the given bounds.
func getExplodeImage(width int, height int, frame int, max int) string {
// Predefine the explosion symbols
explosionChars := []rune{'*', '.', '@', '#', '&', '+', '%'}
// Initialize a buffer to build our string
var buf bytes.Buffer
// Initialize RNG seed
random := rand.New(rand.NewSource(time.Now().UnixNano()))
// calculate the center of explosion
centerX, centerY := width/2, height/2
// calculate the max radius (hypotenuse of the view)
maxRadius := math.Hypot(float64(centerX), float64(centerY))
// calculate frame as a proportion of max, apply square root to create the non-linear effect
progress := math.Sqrt(float64(frame) / float64(max))
// calculate radius of explosion according to frame and max
radius := progress * maxRadius * 2
// introduce a new radius for the inner boundary of the explosion (the shockwave effect)
var innerRadius float64
if progress > 0.5 {
innerRadius = (progress - 0.5) * 2 * maxRadius
}
for y := range height {
for x := range width {
// calculate distance from center, scale x by 2 to compensate for character aspect ratio
distance := math.Hypot(float64(x-centerX), float64(y-centerY)*2)
// if distance is less than radius and greater than innerRadius, draw explosion char
if distance <= radius && distance >= innerRadius {
// Make placement random and less likely as explosion progresses
if random.Float64() > progress {
// Pick a random explosion char
char := explosionChars[random.Intn(len(explosionChars))]
buf.WriteRune(char)
} else {
buf.WriteRune(' ')
}
} else {
// If not explosion, then it's empty space
buf.WriteRune(' ')
}
}
// End of line
if y < height-1 {
buf.WriteRune('\n')
}
}
return buf.String()
}