github.com/andrewrech/lazygit@v0.8.1/pkg/gui/files_panel.go (about)

     1  package gui
     2  
     3  import (
     4  
     5  	// "io"
     6  	// "io/ioutil"
     7  
     8  	// "strings"
     9  
    10  	"fmt"
    11  	"strings"
    12  
    13  	"github.com/fatih/color"
    14  	"github.com/jesseduffield/gocui"
    15  	"github.com/jesseduffield/lazygit/pkg/commands"
    16  	"github.com/jesseduffield/lazygit/pkg/utils"
    17  )
    18  
    19  // list panel functions
    20  
    21  func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
    22  	selectedLine := gui.State.Panels.Files.SelectedLine
    23  	if selectedLine == -1 {
    24  		return &commands.File{}, gui.Errors.ErrNoFiles
    25  	}
    26  
    27  	return gui.State.Files[selectedLine], nil
    28  }
    29  
    30  func (gui *Gui) handleFilesFocus(g *gocui.Gui, v *gocui.View) error {
    31  	if gui.popupPanelFocused() {
    32  		return nil
    33  	}
    34  
    35  	cx, cy := v.Cursor()
    36  	_, oy := v.Origin()
    37  
    38  	prevSelectedLine := gui.State.Panels.Files.SelectedLine
    39  	newSelectedLine := cy - oy
    40  
    41  	if newSelectedLine > len(gui.State.Files)-1 || len(utils.Decolorise(gui.State.Files[newSelectedLine].DisplayString)) < cx {
    42  		return gui.handleFileSelect(gui.g, v, false)
    43  	}
    44  
    45  	gui.State.Panels.Files.SelectedLine = newSelectedLine
    46  
    47  	if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() {
    48  		return gui.handleFilePress(gui.g, v)
    49  	} else {
    50  		return gui.handleFileSelect(gui.g, v, true)
    51  	}
    52  }
    53  
    54  func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error {
    55  	if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
    56  		return err
    57  	}
    58  
    59  	file, err := gui.getSelectedFile(g)
    60  	if err != nil {
    61  		if err != gui.Errors.ErrNoFiles {
    62  			return err
    63  		}
    64  		return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
    65  	}
    66  
    67  	if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), v); err != nil {
    68  		return err
    69  	}
    70  
    71  	if file.HasInlineMergeConflicts {
    72  		return gui.refreshMergePanel()
    73  	}
    74  
    75  	content := gui.GitCommand.Diff(file, false)
    76  	if alreadySelected {
    77  		g.Update(func(*gocui.Gui) error {
    78  			return gui.setViewContent(gui.g, gui.getMainView(), content)
    79  		})
    80  		return nil
    81  	}
    82  	return gui.renderString(g, "main", content)
    83  }
    84  
    85  func (gui *Gui) refreshFiles() error {
    86  	selectedFile, _ := gui.getSelectedFile(gui.g)
    87  
    88  	filesView := gui.getFilesView()
    89  	if filesView == nil {
    90  		// if the filesView hasn't been instantiated yet we just return
    91  		return nil
    92  	}
    93  	if err := gui.refreshStateFiles(); err != nil {
    94  		return err
    95  	}
    96  
    97  	gui.g.Update(func(g *gocui.Gui) error {
    98  
    99  		filesView.Clear()
   100  		isFocused := gui.g.CurrentView().Name() == "files"
   101  		list, err := utils.RenderList(gui.State.Files, isFocused)
   102  		if err != nil {
   103  			return err
   104  		}
   105  		fmt.Fprint(filesView, list)
   106  
   107  		if filesView == g.CurrentView() {
   108  			newSelectedFile, _ := gui.getSelectedFile(gui.g)
   109  			alreadySelected := newSelectedFile.Name == selectedFile.Name
   110  			return gui.handleFileSelect(g, filesView, alreadySelected)
   111  		}
   112  		return nil
   113  	})
   114  
   115  	return nil
   116  }
   117  
   118  func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error {
   119  	if gui.popupPanelFocused() {
   120  		return nil
   121  	}
   122  
   123  	panelState := gui.State.Panels.Files
   124  	gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false)
   125  
   126  	return gui.handleFileSelect(gui.g, v, false)
   127  }
   128  
   129  func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
   130  	if gui.popupPanelFocused() {
   131  		return nil
   132  	}
   133  
   134  	panelState := gui.State.Panels.Files
   135  	gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true)
   136  
   137  	return gui.handleFileSelect(gui.g, v, false)
   138  }
   139  
   140  // specific functions
   141  
   142  func (gui *Gui) stagedFiles() []*commands.File {
   143  	files := gui.State.Files
   144  	result := make([]*commands.File, 0)
   145  	for _, file := range files {
   146  		if file.HasStagedChanges {
   147  			result = append(result, file)
   148  		}
   149  	}
   150  	return result
   151  }
   152  
   153  func (gui *Gui) trackedFiles() []*commands.File {
   154  	files := gui.State.Files
   155  	result := make([]*commands.File, 0)
   156  	for _, file := range files {
   157  		if file.Tracked {
   158  			result = append(result, file)
   159  		}
   160  	}
   161  	return result
   162  }
   163  
   164  func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
   165  	file, err := gui.getSelectedFile(g)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	return gui.GitCommand.StageFile(file.Name)
   170  }
   171  
   172  func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
   173  	file, err := gui.getSelectedFile(g)
   174  	if err != nil {
   175  		if err != gui.Errors.ErrNoFiles {
   176  			return err
   177  		}
   178  		return nil
   179  	}
   180  	if file.HasInlineMergeConflicts {
   181  		return gui.handleSwitchToMerge(g, v)
   182  	}
   183  	if !file.HasUnstagedChanges || file.HasMergeConflicts {
   184  		return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
   185  	}
   186  	if err := gui.changeContext("main", "staging"); err != nil {
   187  		return err
   188  	}
   189  	if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
   190  		return err
   191  	}
   192  	return gui.refreshStagingPanel()
   193  }
   194  
   195  func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
   196  	file, err := gui.getSelectedFile(g)
   197  	if err != nil {
   198  		if err == gui.Errors.ErrNoFiles {
   199  			return nil
   200  		}
   201  		return err
   202  	}
   203  
   204  	if file.HasInlineMergeConflicts {
   205  		return gui.handleSwitchToMerge(g, v)
   206  	}
   207  
   208  	if file.HasUnstagedChanges {
   209  		gui.GitCommand.StageFile(file.Name)
   210  	} else {
   211  		gui.GitCommand.UnStageFile(file.Name, file.Tracked)
   212  	}
   213  
   214  	if err := gui.refreshFiles(); err != nil {
   215  		return err
   216  	}
   217  
   218  	return gui.handleFileSelect(g, v, true)
   219  }
   220  
   221  func (gui *Gui) allFilesStaged() bool {
   222  	for _, file := range gui.State.Files {
   223  		if file.HasUnstagedChanges {
   224  			return false
   225  		}
   226  	}
   227  	return true
   228  }
   229  
   230  func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
   231  	var err error
   232  	if gui.allFilesStaged() {
   233  		err = gui.GitCommand.UnstageAll()
   234  	} else {
   235  		err = gui.GitCommand.StageAll()
   236  	}
   237  	if err != nil {
   238  		_ = gui.createErrorPanel(g, err.Error())
   239  	}
   240  
   241  	if err := gui.refreshFiles(); err != nil {
   242  		return err
   243  	}
   244  
   245  	return gui.handleFileSelect(g, v, false)
   246  }
   247  
   248  func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
   249  	file, err := gui.getSelectedFile(g)
   250  	if err != nil {
   251  		if err == gui.Errors.ErrNoFiles {
   252  			return nil
   253  		}
   254  		return err
   255  	}
   256  	if !file.HasUnstagedChanges {
   257  		return gui.createErrorPanel(g, gui.Tr.SLocalize("FileHasNoUnstagedChanges"))
   258  	}
   259  	if !file.Tracked {
   260  		return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd"))
   261  	}
   262  
   263  	gui.SubProcess = gui.GitCommand.AddPatch(file.Name)
   264  	return gui.Errors.ErrSubProcess
   265  }
   266  
   267  func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
   268  	file, err := gui.getSelectedFile(g)
   269  	if err != nil {
   270  		return gui.createErrorPanel(g, err.Error())
   271  	}
   272  	if file.Tracked {
   273  		return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles"))
   274  	}
   275  	if err := gui.GitCommand.Ignore(file.Name); err != nil {
   276  		return gui.createErrorPanel(g, err.Error())
   277  	}
   278  	return gui.refreshFiles()
   279  }
   280  
   281  func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
   282  	skipHookPreifx := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
   283  	if skipHookPreifx == "" {
   284  		return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
   285  	}
   286  
   287  	if err := gui.renderString(g, "commitMessage", skipHookPreifx); err != nil {
   288  		return err
   289  	}
   290  	if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
   291  		return err
   292  	}
   293  
   294  	return gui.handleCommitPress(g, filesView)
   295  }
   296  
   297  func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
   298  	if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
   299  		return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
   300  	}
   301  	commitMessageView := gui.getCommitMessageView()
   302  	g.Update(func(g *gocui.Gui) error {
   303  		g.SetViewOnTop("commitMessage")
   304  		gui.switchFocus(g, filesView, commitMessageView)
   305  		gui.RenderCommitLength()
   306  		return nil
   307  	})
   308  	return nil
   309  }
   310  
   311  func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
   312  	if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
   313  		return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
   314  	}
   315  	if len(gui.State.Commits) == 0 {
   316  		return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
   317  	}
   318  
   319  	title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
   320  	question := gui.Tr.SLocalize("SureToAmend")
   321  
   322  	return gui.createConfirmationPanel(g, filesView, title, question, func(g *gocui.Gui, v *gocui.View) error {
   323  		ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
   324  		if err != nil {
   325  			return err
   326  		}
   327  		if !ok {
   328  			return nil
   329  		}
   330  
   331  		return gui.refreshSidePanels(g)
   332  	}, nil)
   333  }
   334  
   335  // handleCommitEditorPress - handle when the user wants to commit changes via
   336  // their editor rather than via the popup panel
   337  func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
   338  	if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
   339  		return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
   340  	}
   341  	gui.PrepareSubProcess(g, "git", "commit")
   342  	return nil
   343  }
   344  
   345  // PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
   346  func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) {
   347  	gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess()
   348  	g.Update(func(g *gocui.Gui) error {
   349  		return gui.Errors.ErrSubProcess
   350  	})
   351  }
   352  
   353  func (gui *Gui) editFile(filename string) error {
   354  	_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
   355  	return err
   356  }
   357  
   358  func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
   359  	file, err := gui.getSelectedFile(g)
   360  	if err != nil {
   361  		return gui.createErrorPanel(gui.g, err.Error())
   362  	}
   363  
   364  	return gui.editFile(file.Name)
   365  }
   366  
   367  func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
   368  	file, err := gui.getSelectedFile(g)
   369  	if err != nil {
   370  		return gui.createErrorPanel(gui.g, err.Error())
   371  	}
   372  	return gui.openFile(file.Name)
   373  }
   374  
   375  func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
   376  	return gui.refreshFiles()
   377  }
   378  
   379  func (gui *Gui) refreshStateFiles() error {
   380  	// get files to stage
   381  	files := gui.GitCommand.GetStatusFiles()
   382  	gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
   383  	gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
   384  	return gui.updateWorkTreeState()
   385  }
   386  
   387  func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
   388  	item, err := gui.getSelectedFile(g)
   389  	if err != nil {
   390  		if err != gui.Errors.ErrNoFiles {
   391  			return "", err
   392  		}
   393  		return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay"))
   394  	}
   395  	if item.Type != "file" {
   396  		return "", gui.renderString(g, "main", gui.Tr.SLocalize("NotAFile"))
   397  	}
   398  	cat, err := gui.GitCommand.CatFile(item.Name)
   399  	if err != nil {
   400  		gui.Log.Error(err)
   401  		return "", gui.renderString(g, "main", err.Error())
   402  	}
   403  	return cat, nil
   404  }
   405  
   406  func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
   407  	if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
   408  		return err
   409  	}
   410  
   411  	go func() {
   412  		unamePassOpend := false
   413  		err := gui.GitCommand.Pull(func(passOrUname string) string {
   414  			unamePassOpend = true
   415  			return gui.waitForPassUname(g, v, passOrUname)
   416  		})
   417  		gui.HandleCredentialsPopup(g, unamePassOpend, err)
   418  	}()
   419  	return nil
   420  }
   421  
   422  func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error {
   423  	if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
   424  		return err
   425  	}
   426  	go func() {
   427  		unamePassOpend := false
   428  		branchName := gui.State.Branches[0].Name
   429  		err := gui.GitCommand.Push(branchName, force, func(passOrUname string) string {
   430  			unamePassOpend = true
   431  			return gui.waitForPassUname(g, v, passOrUname)
   432  		})
   433  		gui.HandleCredentialsPopup(g, unamePassOpend, err)
   434  	}()
   435  	return nil
   436  }
   437  
   438  func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
   439  	// if we have pullables we'll ask if the user wants to force push
   440  	_, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
   441  	if pullables == "?" || pullables == "0" {
   442  		return gui.pushWithForceFlag(g, v, false)
   443  	}
   444  	err := gui.createConfirmationPanel(g, nil, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
   445  		return gui.pushWithForceFlag(g, v, true)
   446  	}, nil)
   447  	return err
   448  }
   449  
   450  func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
   451  	file, err := gui.getSelectedFile(g)
   452  	if err != nil {
   453  		if err != gui.Errors.ErrNoFiles {
   454  			return gui.createErrorPanel(gui.g, err.Error())
   455  		}
   456  		return nil
   457  	}
   458  	if !file.HasInlineMergeConflicts {
   459  		return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
   460  	}
   461  	if err := gui.changeContext("main", "merging"); err != nil {
   462  		return err
   463  	}
   464  	if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
   465  		return err
   466  	}
   467  	return gui.refreshMergePanel()
   468  }
   469  
   470  func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
   471  	if err := gui.GitCommand.AbortMerge(); err != nil {
   472  		return gui.createErrorPanel(g, err.Error())
   473  	}
   474  	gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
   475  	gui.refreshStatus(g)
   476  	return gui.refreshFiles()
   477  }
   478  
   479  func (gui *Gui) openFile(filename string) error {
   480  	if err := gui.OSCommand.OpenFile(filename); err != nil {
   481  		return gui.createErrorPanel(gui.g, err.Error())
   482  	}
   483  	return nil
   484  }
   485  
   486  func (gui *Gui) anyFilesWithMergeConflicts() bool {
   487  	for _, file := range gui.State.Files {
   488  		if file.HasMergeConflicts {
   489  			return true
   490  		}
   491  	}
   492  	return false
   493  }
   494  
   495  type discardOption struct {
   496  	handler     func(fileName *commands.File) error
   497  	description string
   498  }
   499  
   500  type discardAllOption struct {
   501  	handler     func() error
   502  	description string
   503  	command     string
   504  }
   505  
   506  // GetDisplayStrings is a function.
   507  func (r *discardOption) GetDisplayStrings(isFocused bool) []string {
   508  	return []string{r.description}
   509  }
   510  
   511  // GetDisplayStrings is a function.
   512  func (r *discardAllOption) GetDisplayStrings(isFocused bool) []string {
   513  	return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
   514  }
   515  
   516  func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
   517  	file, err := gui.getSelectedFile(g)
   518  	if err != nil {
   519  		if err != gui.Errors.ErrNoFiles {
   520  			return err
   521  		}
   522  		return nil
   523  	}
   524  
   525  	options := []*discardOption{
   526  		{
   527  			description: gui.Tr.SLocalize("discardAllChanges"),
   528  			handler: func(file *commands.File) error {
   529  				return gui.GitCommand.DiscardAllFileChanges(file)
   530  			},
   531  		},
   532  		{
   533  			description: gui.Tr.SLocalize("cancel"),
   534  			handler: func(file *commands.File) error {
   535  				return nil
   536  			},
   537  		},
   538  	}
   539  
   540  	if file.HasStagedChanges && file.HasUnstagedChanges {
   541  		discardUnstagedChanges := &discardOption{
   542  			description: gui.Tr.SLocalize("discardUnstagedChanges"),
   543  			handler: func(file *commands.File) error {
   544  				return gui.GitCommand.DiscardUnstagedFileChanges(file)
   545  			},
   546  		}
   547  
   548  		options = append(options[:1], append([]*discardOption{discardUnstagedChanges}, options[1:]...)...)
   549  	}
   550  
   551  	handleMenuPress := func(index int) error {
   552  		file, err := gui.getSelectedFile(g)
   553  		if err != nil {
   554  			return err
   555  		}
   556  
   557  		if err := options[index].handler(file); err != nil {
   558  			return err
   559  		}
   560  
   561  		return gui.refreshFiles()
   562  	}
   563  
   564  	return gui.createMenu(file.Name, options, len(options), handleMenuPress)
   565  }
   566  
   567  func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
   568  	options := []*discardAllOption{
   569  		{
   570  			description: gui.Tr.SLocalize("discardAllChangesToAllFiles"),
   571  			command:     "reset --hard HEAD && git clean -fd",
   572  			handler: func() error {
   573  				return gui.GitCommand.ResetAndClean()
   574  			},
   575  		},
   576  		{
   577  			description: gui.Tr.SLocalize("discardAnyUnstagedChanges"),
   578  			command:     "git checkout -- .",
   579  			handler: func() error {
   580  				return gui.GitCommand.DiscardAnyUnstagedFileChanges()
   581  			},
   582  		},
   583  		{
   584  			description: gui.Tr.SLocalize("discardUntrackedFiles"),
   585  			command:     "git clean -fd",
   586  			handler: func() error {
   587  				return gui.GitCommand.RemoveUntrackedFiles()
   588  			},
   589  		},
   590  		{
   591  			description: gui.Tr.SLocalize("softReset"),
   592  			command:     "git reset --soft HEAD",
   593  			handler: func() error {
   594  				return gui.GitCommand.ResetSoftHead()
   595  			},
   596  		},
   597  		{
   598  			description: gui.Tr.SLocalize("hardReset"),
   599  			command:     "git reset --hard HEAD",
   600  			handler: func() error {
   601  				return gui.GitCommand.ResetHardHead()
   602  			},
   603  		},
   604  		{
   605  			description: gui.Tr.SLocalize("cancel"),
   606  			handler: func() error {
   607  				return nil
   608  			},
   609  		},
   610  	}
   611  
   612  	handleMenuPress := func(index int) error {
   613  		if err := options[index].handler(); err != nil {
   614  			return err
   615  		}
   616  
   617  		return gui.refreshFiles()
   618  	}
   619  
   620  	return gui.createMenu("", options, len(options), handleMenuPress)
   621  }
   622  
   623  func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
   624  	return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), func(g *gocui.Gui, v *gocui.View) error {
   625  		command := gui.trimmedContent(v)
   626  		gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
   627  		return gui.Errors.ErrSubProcess
   628  	})
   629  }
   630  
   631  type stashOption struct {
   632  	description string
   633  	handler     func() error
   634  }
   635  
   636  // GetDisplayStrings is a function.
   637  func (o *stashOption) GetDisplayStrings(isFocused bool) []string {
   638  	return []string{o.description}
   639  }
   640  
   641  func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
   642  	options := []*stashOption{
   643  		{
   644  			description: gui.Tr.SLocalize("stashAllChanges"),
   645  			handler: func() error {
   646  				return gui.handleStashSave(gui.GitCommand.StashSave)
   647  			},
   648  		},
   649  		{
   650  			description: gui.Tr.SLocalize("stashStagedChanges"),
   651  			handler: func() error {
   652  				return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
   653  			},
   654  		},
   655  		{
   656  			description: gui.Tr.SLocalize("cancel"),
   657  			handler: func() error {
   658  				return nil
   659  			},
   660  		},
   661  	}
   662  
   663  	handleMenuPress := func(index int) error {
   664  		return options[index].handler()
   665  	}
   666  
   667  	return gui.createMenu(gui.Tr.SLocalize("stashOptions"), options, len(options), handleMenuPress)
   668  }
   669  
   670  func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
   671  	return gui.handleStashSave(gui.GitCommand.StashSave)
   672  }