mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	Add explosion animation when nuking working tree
I've been thinking about this for a while: I think it looks really cool if nuking your working tree actually results in a nuke animation. So I've added an opt-out config for it
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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{ | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
|   | ||||
							
								
								
									
										45
									
								
								pkg/integration/tests/demo/nuke_working_tree.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								pkg/integration/tests/demo/nuke_working_tree.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| 			}) | ||||
| 	}, | ||||
| }) | ||||
| @@ -93,6 +93,7 @@ var tests = []*components.IntegrationTest{ | ||||
| 	demo.CherryPick, | ||||
| 	demo.CommitAndPush, | ||||
| 	demo.InteractiveRebase, | ||||
| 	demo.NukeWorkingTree, | ||||
| 	diff.Diff, | ||||
| 	diff.DiffAndApplyPatch, | ||||
| 	diff.DiffCommits, | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user