mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-11 20:36:03 +02:00
Now that we have an error handler set, we can simply let them bubble up all the way to gocui.
260 lines
6.8 KiB
Go
260 lines
6.8 KiB
Go
package controllers
|
|
|
|
import (
|
|
"bytes"
|
|
"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()
|
|
}
|
|
|
|
return self.c.Refresh(
|
|
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
|
|
)
|
|
},
|
|
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
|
|
}
|
|
|
|
return self.c.Refresh(
|
|
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
|
|
)
|
|
},
|
|
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
|
|
}
|
|
|
|
return self.c.Refresh(
|
|
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
|
|
)
|
|
},
|
|
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 self.c.ErrorMsg(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
|
|
}
|
|
|
|
return self.c.Refresh(
|
|
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
|
|
)
|
|
},
|
|
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
|
|
}
|
|
|
|
return self.c.Refresh(
|
|
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
|
|
)
|
|
},
|
|
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
|
|
}
|
|
|
|
return self.c.Refresh(
|
|
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
|
|
)
|
|
},
|
|
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
|
|
}
|
|
|
|
return self.c.Refresh(
|
|
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
|
|
)
|
|
},
|
|
Key: 'h',
|
|
},
|
|
}
|
|
|
|
return self.c.Menu(types.CreateMenuOptions{Title: "", Items: menuItems})
|
|
}
|
|
|
|
func (self *FilesController) animateExplosion() {
|
|
self.Explode(self.c.Views().Files, func() {
|
|
err := self.c.PostRefreshUpdate(self.c.Contexts().Files)
|
|
if err != nil {
|
|
self.c.Log.Error(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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() + 1
|
|
styles := []style.TextStyle{
|
|
style.FgLightWhite.SetBold(),
|
|
style.FgYellow.SetBold(),
|
|
style.FgRed.SetBold(),
|
|
style.FgBlue.SetBold(),
|
|
style.FgBlack.SetBold(),
|
|
}
|
|
|
|
self.c.OnWorker(func(_ gocui.Task) {
|
|
max := 25
|
|
for i := 0; i < max; i++ {
|
|
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
|
|
})
|
|
})
|
|
}
|
|
|
|
// 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 := 0; y < height; y++ {
|
|
for x := 0; x < width; x++ {
|
|
// 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()
|
|
}
|