package gui import ( "fmt" "strings" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" ) func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func() error { return func() error { if err := gui.closeConfirmationPrompt(handlersManageFocus); err != nil { return err } if function != nil { if err := function(); err != nil { return gui.c.Error(err) } } return nil } } func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, function func(string) error, getResponse func() string) func() error { return func() error { if err := gui.closeConfirmationPrompt(handlersManageFocus); err != nil { return err } if function != nil { if err := function(getResponse()); err != nil { return gui.c.Error(err) } } return nil } } func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error { gui.Mutexes.PopupMutex.Lock() gui.State.CurrentPopupOpts = nil gui.Mutexes.PopupMutex.Unlock() // we've already closed it so we can just return if !gui.Views.Confirmation.Visible { return nil } if !handlersManageFocus { if err := gui.c.PopContext(); err != nil { return err } } gui.clearConfirmationViewKeyBindings() gui.Views.Confirmation.Visible = false gui.Views.Suggestions.Visible = false return nil } func (gui *Gui) 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 += len(line)/width + 1 } } else { lineCount = len(lines) } return lineCount } func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) { panelWidth := gui.getConfirmationPanelWidth() panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) return gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight) } func (gui *Gui) getConfirmationPanelDimensionsForContentHeight(contentHeight int) (int, int, int, int) { panelWidth := gui.getConfirmationPanelWidth() return gui.getConfirmationPanelDimensionsAux(panelWidth, contentHeight) } func (gui *Gui) getConfirmationPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) { width, height := gui.g.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 (gui *Gui) getConfirmationPanelWidth() int { width, _ := gui.g.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 (gui *Gui) prepareConfirmationPanel( title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion, editable bool, mask bool, ) error { x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt) // calling SetView on an existing view returns the same view, so I'm not bothering // to reassign to gui.Views.Confirmation _, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) if err != nil { return err } gui.Views.Confirmation.HasLoader = hasLoader if hasLoader { gui.g.StartTicking() } gui.Views.Confirmation.Title = title // for now we do not support wrapping in our editor gui.Views.Confirmation.Wrap = !editable gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor gui.Views.Confirmation.Mask = runeForMask(mask) gui.findSuggestions = findSuggestionsFunc if findSuggestionsFunc != nil { suggestionsViewHeight := 11 suggestionsView, err := gui.g.SetView("suggestions", x0, y1+1, x1, y1+suggestionsViewHeight, 0) if err != nil { return err } suggestionsView.Wrap = false suggestionsView.FgColor = theme.GocuiDefaultTextColor gui.setSuggestions(findSuggestionsFunc("")) suggestionsView.Visible = true suggestionsView.Title = fmt.Sprintf(gui.c.Tr.SuggestionsTitle, gui.c.UserConfig.Keybinding.Universal.TogglePanel) } return nil } func runeForMask(mask bool) rune { if mask { return '*' } return 0 } func (gui *Gui) createPopupPanel(opts types.CreatePopupPanelOpts) error { gui.Mutexes.PopupMutex.Lock() defer gui.Mutexes.PopupMutex.Unlock() // 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 if gui.State.CurrentPopupOpts != nil && !gui.State.CurrentPopupOpts.HasLoader { gui.Log.Error("ignoring create popup panel because a popup panel is already open") return nil } // remove any previous keybindings gui.clearConfirmationViewKeyBindings() err := gui.prepareConfirmationPanel( opts.Title, opts.Prompt, opts.HasLoader, opts.FindSuggestionsFunc, opts.Editable, opts.Mask, ) if err != nil { return err } confirmationView := gui.Views.Confirmation confirmationView.Editable = opts.Editable confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor) if opts.Editable { textArea := confirmationView.TextArea textArea.Clear() textArea.TypeString(opts.Prompt) confirmationView.RenderTextArea() } else { if err := gui.renderString(confirmationView, opts.Prompt); err != nil { return err } } if err := gui.setKeyBindings(opts); err != nil { return err } gui.State.CurrentPopupOpts = &opts return gui.c.PushContext(gui.State.Contexts.Confirmation) } func (gui *Gui) setKeyBindings(opts types.CreatePopupPanelOpts) error { actions := utils.ResolvePlaceholderString( gui.c.Tr.CloseConfirm, map[string]string{ "keyBindClose": "esc", "keyBindConfirm": "enter", }, ) _ = gui.renderString(gui.Views.Options, actions) var onConfirm func() error if opts.HandleConfirmPrompt != nil { onConfirm = gui.wrappedPromptConfirmationFunction(opts.HandlersManageFocus, opts.HandleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() }) } else { onConfirm = gui.wrappedConfirmationFunction(opts.HandlersManageFocus, opts.HandleConfirm) } keybindingConfig := gui.c.UserConfig.Keybinding onSuggestionConfirm := gui.wrappedPromptConfirmationFunction( opts.HandlersManageFocus, opts.HandleConfirmPrompt, gui.getSelectedSuggestionValue, ) bindings := []*types.Binding{ { ViewName: "confirmation", Contexts: []string{string(context.CONFIRMATION_CONTEXT_KEY)}, Key: gui.getKey(keybindingConfig.Universal.Confirm), Handler: onConfirm, }, { ViewName: "confirmation", Contexts: []string{string(context.CONFIRMATION_CONTEXT_KEY)}, Key: gui.getKey(keybindingConfig.Universal.ConfirmAlt1), Handler: onConfirm, }, { ViewName: "confirmation", Contexts: []string{string(context.CONFIRMATION_CONTEXT_KEY)}, Key: gui.getKey(keybindingConfig.Universal.Return), Handler: gui.wrappedConfirmationFunction(opts.HandlersManageFocus, opts.HandleClose), }, { ViewName: "confirmation", Contexts: []string{string(context.CONFIRMATION_CONTEXT_KEY)}, Key: gui.getKey(keybindingConfig.Universal.TogglePanel), Handler: func() error { if len(gui.State.Suggestions) > 0 { return gui.replaceContext(gui.State.Contexts.Suggestions) } return nil }, }, { ViewName: "suggestions", Contexts: []string{string(context.SUGGESTIONS_CONTEXT_KEY)}, Key: gui.getKey(keybindingConfig.Universal.Confirm), Handler: onSuggestionConfirm, }, { ViewName: "suggestions", Contexts: []string{string(context.SUGGESTIONS_CONTEXT_KEY)}, Key: gui.getKey(keybindingConfig.Universal.ConfirmAlt1), Handler: onSuggestionConfirm, }, { ViewName: "suggestions", Contexts: []string{string(context.SUGGESTIONS_CONTEXT_KEY)}, Key: gui.getKey(keybindingConfig.Universal.Return), Handler: gui.wrappedConfirmationFunction(opts.HandlersManageFocus, opts.HandleClose), }, { ViewName: "suggestions", Contexts: []string{string(context.SUGGESTIONS_CONTEXT_KEY)}, Key: gui.getKey(keybindingConfig.Universal.TogglePanel), Handler: func() error { return gui.replaceContext(gui.State.Contexts.Confirmation) }, }, } for _, binding := range bindings { if err := gui.SetKeybinding(binding); err != nil { return err } } return nil } func (gui *Gui) clearConfirmationViewKeyBindings() { keybindingConfig := gui.c.UserConfig.Keybinding _ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone) _ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone) _ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone) _ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone) _ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone) _ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone) } func (gui *Gui) refreshSuggestions() { gui.suggestionsAsyncHandler.Do(func() func() { suggestions := gui.findSuggestions(gui.c.GetPromptInput()) return func() { gui.setSuggestions(suggestions) } }) } func (gui *Gui) handleAskFocused() error { keybindingConfig := gui.c.UserConfig.Keybinding message := utils.ResolvePlaceholderString( gui.c.Tr.CloseConfirm, map[string]string{ "keyBindClose": gui.getKeyDisplay(keybindingConfig.Universal.Return), "keyBindConfirm": gui.getKeyDisplay(keybindingConfig.Universal.Confirm), }, ) return gui.renderString(gui.Views.Options, message) }