mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-03 00:57:52 +02:00
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.
267 lines
6.8 KiB
Go
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()
|
|
}
|