diff --git a/docs/Config.md b/docs/Config.md index 447ccae5f..a4987268d 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -70,6 +70,7 @@ gui: splitDiff: 'auto' # one of 'auto' | 'always' skipRewordInEditorWarning: false # for skipping the confirmation before launching the reword editor border: 'single' # one of 'single' | 'double' | 'rounded' | 'hidden' + animateExplosion: true # shows an explosion animation when nuking the working tree git: paging: colorArg: always diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 0426086fc..050ac7099 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -56,6 +56,7 @@ type GuiConfig struct { SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"` WindowSize string `yaml:"windowSize"` Border string `yaml:"border"` + AnimateExplosion bool `yaml:"animateExplosion"` } type ThemeConfig struct { @@ -454,6 +455,7 @@ func GetDefaultConfig() *UserConfig { SplitDiff: "auto", SkipRewordInEditorWarning: false, Border: "single", + AnimateExplosion: true, }, Git: GitConfig{ Paging: PagingConfig{ diff --git a/pkg/gui/controllers/workspace_reset_controller.go b/pkg/gui/controllers/workspace_reset_controller.go index db247ae25..195a9699e 100644 --- a/pkg/gui/controllers/workspace_reset_controller.go +++ b/pkg/gui/controllers/workspace_reset_controller.go @@ -1,8 +1,13 @@ 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" ) @@ -29,6 +34,10 @@ func (self *FilesController) createResetMenu() error { return self.c.Error(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', @@ -135,3 +144,102 @@ func (self *FilesController) createResetMenu() error { 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.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 + rand.Seed(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 rand.Float64() > progress { + // Pick a random explosion char + char := explosionChars[rand.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() +} diff --git a/pkg/integration/tests/demo/nuke_working_tree.go b/pkg/integration/tests/demo/nuke_working_tree.go new file mode 100644 index 000000000..d69b06879 --- /dev/null +++ b/pkg/integration/tests/demo/nuke_working_tree.go @@ -0,0 +1,45 @@ +package demo + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var NukeWorkingTree = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Nuke the working tree", + ExtraCmdArgs: []string{"status"}, + Skip: false, + IsDemo: true, + SetupConfig: func(config *config.AppConfig) { + // No idea why I had to use version 2: it should be using my own computer's + // font and the one iterm uses is version 3. + config.UserConfig.Gui.NerdFontsVersion = "2" + config.UserConfig.Gui.AnimateExplosion = true + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("blah") + shell.CreateFile("controllers/red_controller.rb", "") + shell.CreateFile("controllers/green_controller.rb", "") + shell.CreateFileAndAdd("controllers/blue_controller.rb", "") + shell.CreateFile("controllers/README.md", "") + shell.CreateFileAndAdd("views/helpers/list.rb", "") + shell.CreateFile("views/helpers/sort.rb", "") + shell.CreateFileAndAdd("views/users_view.rb", "") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.SetCaptionPrefix("Nuke the working tree") + + t.Views().Files(). + IsFocused(). + Wait(1000). + Press(keys.Files.ViewResetOptions). + Tap(func() { + t.Wait(1000) + + t.ExpectPopup().Menu(). + Title(Equals("")). + Select(Contains("Nuke working tree")). + Confirm() + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 4d19c5d84..27cb3513e 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -93,6 +93,7 @@ var tests = []*components.IntegrationTest{ demo.CherryPick, demo.CommitAndPush, demo.InteractiveRebase, + demo.NukeWorkingTree, diff.Diff, diff.DiffAndApplyPatch, diff.DiffCommits, diff --git a/test/default_test_config/config.yml b/test/default_test_config/config.yml index 04f7422cb..4f481f0bc 100644 --- a/test/default_test_config/config.yml +++ b/test/default_test_config/config.yml @@ -14,6 +14,7 @@ gui: - reverse # Not important in tests but it creates clutter in demos showRandomTip: false + animateExplosion: false # takes too long git: # We don't want to run any periodic background git commands because it'll introduce race conditions and flakiness. # If we need to refresh something from within the test (which should only really happen if we've invoked a