1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-12 11:15:00 +02:00
lazygit/pkg/gui/controllers/helpers/confirmation_helper.go
Jesse Duffield 373f24c80f Fix crash on empty menu
When a menu is empty (e.g. due to filtering) we shouldn't crash on focus or selection
2023-07-20 21:05:52 +10:00

357 lines
11 KiB
Go

package helpers
import (
goContext "context"
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/mattn/go-runewidth"
)
type ConfirmationHelper struct {
c *HelperCommon
}
func NewConfirmationHelper(c *HelperCommon) *ConfirmationHelper {
return &ConfirmationHelper{
c: c,
}
}
// This file is for the rendering of confirmation panels along with setting and handling associated
// keybindings.
func (self *ConfirmationHelper) wrappedConfirmationFunction(cancel goContext.CancelFunc, function func() error) func() error {
return func() error {
cancel()
if err := self.c.PopContext(); err != nil {
return err
}
if function != nil {
if err := function(); err != nil {
return self.c.Error(err)
}
}
return nil
}
}
func (self *ConfirmationHelper) wrappedPromptConfirmationFunction(cancel goContext.CancelFunc, function func(string) error, getResponse func() string) func() error {
return self.wrappedConfirmationFunction(cancel, func() error {
return function(getResponse())
})
}
func (self *ConfirmationHelper) DeactivateConfirmationPrompt() {
self.c.Mutexes().PopupMutex.Lock()
self.c.State().GetRepoState().SetCurrentPopupOpts(nil)
self.c.Mutexes().PopupMutex.Unlock()
self.c.Views().Confirmation.Visible = false
self.c.Views().Suggestions.Visible = false
self.clearConfirmationViewKeyBindings()
}
func getMessageHeight(wrap bool, message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
// if we need to wrap, calculate height to fit content within view's width
if wrap {
for _, line := range lines {
lineCount += runewidth.StringWidth(line)/width + 1
}
} else {
lineCount = len(lines)
}
return lineCount
}
func (self *ConfirmationHelper) getPopupPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
panelWidth := self.getPopupPanelWidth()
panelHeight := getMessageHeight(wrap, prompt, panelWidth)
return self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
}
func (self *ConfirmationHelper) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return self.getPopupPanelDimensionsAux(panelWidth, contentHeight)
}
func (self *ConfirmationHelper) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
width, height := self.c.GocuiGui().Size()
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
}
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func (self *ConfirmationHelper) getPopupPanelWidth() int {
width, _ := self.c.GocuiGui().Size()
// we want a minimum width up to a point, then we do it based on ratio.
panelWidth := 4 * width / 7
minWidth := 80
if panelWidth < minWidth {
if width-2 < minWidth {
panelWidth = width - 2
} else {
panelWidth = minWidth
}
}
return panelWidth
}
func (self *ConfirmationHelper) prepareConfirmationPanel(
ctx goContext.Context,
opts types.ConfirmOpts,
) error {
self.c.Views().Confirmation.HasLoader = opts.HasLoader
if opts.HasLoader {
self.c.GocuiGui().StartTicking(ctx)
}
self.c.Views().Confirmation.Title = opts.Title
// for now we do not support wrapping in our editor
self.c.Views().Confirmation.Wrap = !opts.Editable
self.c.Views().Confirmation.FgColor = theme.GocuiDefaultTextColor
self.c.Views().Confirmation.Mask = runeForMask(opts.Mask)
_ = self.c.Views().Confirmation.SetOrigin(0, 0)
suggestionsContext := self.c.Contexts().Suggestions
suggestionsContext.State.FindSuggestions = opts.FindSuggestionsFunc
if opts.FindSuggestionsFunc != nil {
suggestionsView := self.c.Views().Suggestions
suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor
suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel)
}
self.ResizeConfirmationPanel()
return nil
}
func runeForMask(mask bool) rune {
if mask {
return '*'
}
return 0
}
func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts types.CreatePopupPanelOpts) error {
self.c.Mutexes().PopupMutex.Lock()
defer self.c.Mutexes().PopupMutex.Unlock()
ctx, cancel := goContext.WithCancel(ctx)
// we don't allow interruptions of non-loader popups in case we get stuck somehow
// e.g. a credentials popup never gets its required user input so a process hangs
// forever.
// The proper solution is to have a queue of popup options
currentPopupOpts := self.c.State().GetRepoState().GetCurrentPopupOpts()
if currentPopupOpts != nil && !currentPopupOpts.HasLoader {
self.c.Log.Error("ignoring create popup panel because a popup panel is already open")
cancel()
return nil
}
// remove any previous keybindings
self.clearConfirmationViewKeyBindings()
err := self.prepareConfirmationPanel(
ctx,
types.ConfirmOpts{
Title: opts.Title,
Prompt: opts.Prompt,
HasLoader: opts.HasLoader,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Editable: opts.Editable,
Mask: opts.Mask,
})
if err != nil {
cancel()
return err
}
confirmationView := self.c.Views().Confirmation
confirmationView.Editable = opts.Editable
if opts.Editable {
textArea := confirmationView.TextArea
textArea.Clear()
textArea.TypeString(opts.Prompt)
self.ResizeConfirmationPanel()
confirmationView.RenderTextArea()
} else {
self.c.ResetViewOrigin(confirmationView)
self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt))
}
if err := self.setKeyBindings(cancel, opts); err != nil {
cancel()
return err
}
self.c.State().GetRepoState().SetCurrentPopupOpts(&opts)
return self.c.PushContext(self.c.Contexts().Confirmation)
}
func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) error {
var onConfirm func() error
if opts.HandleConfirmPrompt != nil {
onConfirm = self.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return self.c.Views().Confirmation.TextArea.GetContent() })
} else {
onConfirm = self.wrappedConfirmationFunction(cancel, opts.HandleConfirm)
}
onSuggestionConfirm := self.wrappedPromptConfirmationFunction(
cancel,
opts.HandleConfirmPrompt,
self.getSelectedSuggestionValue,
)
onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose)
self.c.Contexts().Confirmation.State.OnConfirm = onConfirm
self.c.Contexts().Confirmation.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm
self.c.Contexts().Suggestions.State.OnClose = onClose
return nil
}
func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() {
noop := func() error { return nil }
self.c.Contexts().Confirmation.State.OnConfirm = noop
self.c.Contexts().Confirmation.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnConfirm = noop
self.c.Contexts().Suggestions.State.OnClose = noop
}
func (self *ConfirmationHelper) getSelectedSuggestionValue() string {
selectedSuggestion := self.c.Contexts().Suggestions.GetSelected()
if selectedSuggestion != nil {
return selectedSuggestion.Value
}
return ""
}
func (self *ConfirmationHelper) ResizeConfirmationPanel() {
suggestionsViewHeight := 0
if self.c.Views().Suggestions.Visible {
suggestionsViewHeight = 11
}
panelWidth := self.getPopupPanelWidth()
prompt := self.c.Views().Confirmation.Buffer()
wrap := true
if self.c.Views().Confirmation.Editable {
prompt = self.c.Views().Confirmation.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
_, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
}
func (self *ConfirmationHelper) ResizeCurrentPopupPanel() error {
c := self.c.CurrentContext()
switch c {
case self.c.Contexts().Menu:
self.resizeMenu()
case self.c.Contexts().Confirmation, self.c.Contexts().Suggestions:
self.resizeConfirmationPanel()
case self.c.Contexts().CommitMessage, self.c.Contexts().CommitDescription:
self.ResizeCommitMessagePanels()
}
return nil
}
func (self *ConfirmationHelper) ResizePopupPanel(v *gocui.View, content string) error {
x0, y0, x1, y1 := self.getPopupPanelDimensions(v.Wrap, content)
_, err := self.c.GocuiGui().SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
func (self *ConfirmationHelper) resizeMenu() {
// we want the unfiltered length here so that if we're filtering we don't
// resize the window
itemCount := self.c.Contexts().Menu.UnfilteredLen()
offset := 3
panelWidth := self.getPopupPanelWidth()
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
menuBottom := y1 - offset
_, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0)
tooltipTop := menuBottom + 1
tooltip := ""
selectedItem := self.c.Contexts().Menu.GetSelected()
if selectedItem != nil {
tooltip = selectedItem.Tooltip
}
tooltipHeight := getMessageHeight(true, tooltip, panelWidth) + 2 // plus 2 for the frame
_, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
}
func (self *ConfirmationHelper) resizeConfirmationPanel() {
suggestionsViewHeight := 0
if self.c.Views().Suggestions.Visible {
suggestionsViewHeight = 11
}
panelWidth := self.getPopupPanelWidth()
prompt := self.c.Views().Confirmation.Buffer()
wrap := true
if self.c.Views().Confirmation.Editable {
prompt = self.c.Views().Confirmation.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
_, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
}
func (self *ConfirmationHelper) ResizeCommitMessagePanels() {
panelWidth := self.getPopupPanelWidth()
content := self.c.Views().CommitDescription.TextArea.GetContent()
summaryViewHeight := 3
panelHeight := getMessageHeight(false, content, panelWidth)
minHeight := 7
if panelHeight < minHeight {
panelHeight = minHeight
}
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
_, _ = self.c.GocuiGui().SetView(self.c.Views().CommitMessage.Name(), x0, y0, x1, y0+summaryViewHeight-1, 0)
_, _ = self.c.GocuiGui().SetView(self.c.Views().CommitDescription.Name(), x0, y0+summaryViewHeight, x1, y1+summaryViewHeight, 0)
}
func (self *ConfirmationHelper) IsPopupPanel(viewName string) bool {
return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu"
}
func (self *ConfirmationHelper) IsPopupPanelFocused() bool {
return self.IsPopupPanel(self.c.CurrentContext().GetViewName())
}