mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-19 17:02:18 +03:00
Original commit message of the gocui change: This fixes View.Size, Width and Height to be the correct (outer) size of a view including its frame, and InnerSize/InnerWidth/InnerHeight to be the usable client area exluding the frame. Previously, Size was actually the InnerSize (and a lot of client code used it as such, so these need to be changed to InnerSize). InnerSize, on the other hand, was *one* less than Size (not two, as you would have expected), and in many cases this was made up for at call sites by adding 1 (e.g. in calcRealScrollbarStartEnd, parseInput, and many other places in the lazygit code). There are still some weird things left that I didn't address here: - a view's lower-right coordinates (x1/y1) are one less than you would expect. For example, a view with a 2x2 client area like this: ╭──╮ │ab│ │cd│ ╰──╯ in the top-left corner of the screen (x0 and y0 both zero) has x1/xy at 3, not 4 as would be more natural. - a view without a frame has its coordinates extended by 1 on all sides; to illustrate, the same 2x2 view as before but without a frame, sitting in the top-left corder of the screen, has coordinates x0=-1, y0=-1, x1=2, y1=2. This is highly confusing and unexpected. I left these as they are because they would be even more of a breaking change, and also because they don't have quite as much of an impact on general app code.
260 lines
6.8 KiB
Go
260 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()
|
|
}
|
|
|
|
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 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
|
|
}
|
|
|
|
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() {
|
|
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 := 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
|
|
})
|
|
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()
|
|
}
|