mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-04 10:34:55 +02:00
342 lines
10 KiB
Go
342 lines
10 KiB
Go
package gui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/mattn/go-runewidth"
|
|
)
|
|
|
|
// This file is for the rendering of confirmation panels along with setting and handling associated
|
|
// keybindings.
|
|
|
|
func (gui *Gui) wrappedConfirmationFunction(cancel context.CancelFunc, function func() error) func() error {
|
|
return func() error {
|
|
cancel()
|
|
|
|
if err := gui.c.PopContext(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if function != nil {
|
|
if err := function(); err != nil {
|
|
return gui.c.Error(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) wrappedPromptConfirmationFunction(cancel context.CancelFunc, function func(string) error, getResponse func() string) func() error {
|
|
return func() error {
|
|
cancel()
|
|
|
|
if err := gui.c.PopContext(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if function != nil {
|
|
if err := function(getResponse()); err != nil {
|
|
return gui.c.Error(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) deactivateConfirmationPrompt() {
|
|
gui.Mutexes.PopupMutex.Lock()
|
|
gui.State.CurrentPopupOpts = nil
|
|
gui.Mutexes.PopupMutex.Unlock()
|
|
|
|
gui.Views.Confirmation.Visible = false
|
|
gui.Views.Suggestions.Visible = false
|
|
|
|
gui.clearConfirmationViewKeyBindings()
|
|
}
|
|
|
|
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 += runewidth.StringWidth(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(panelWidth, contentHeight int) (int, int, int, int) {
|
|
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(
|
|
ctx context.Context,
|
|
opts types.ConfirmOpts,
|
|
) error {
|
|
gui.Views.Confirmation.HasLoader = opts.HasLoader
|
|
if opts.HasLoader {
|
|
gui.g.StartTicking(ctx)
|
|
}
|
|
gui.Views.Confirmation.Title = opts.Title
|
|
// for now we do not support wrapping in our editor
|
|
gui.Views.Confirmation.Wrap = !opts.Editable
|
|
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
|
|
gui.Views.Confirmation.Mask = runeForMask(opts.Mask)
|
|
_ = gui.Views.Confirmation.SetOrigin(0, 0)
|
|
|
|
gui.findSuggestions = opts.FindSuggestionsFunc
|
|
if opts.FindSuggestionsFunc != nil {
|
|
suggestionsView := gui.Views.Suggestions
|
|
suggestionsView.Wrap = false
|
|
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
|
gui.setSuggestions(opts.FindSuggestionsFunc(""))
|
|
suggestionsView.Visible = true
|
|
suggestionsView.Title = fmt.Sprintf(gui.c.Tr.SuggestionsTitle, gui.c.UserConfig.Keybinding.Universal.TogglePanel)
|
|
}
|
|
|
|
gui.resizeConfirmationPanel()
|
|
return nil
|
|
}
|
|
|
|
func runeForMask(mask bool) rune {
|
|
if mask {
|
|
return '*'
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPanelOpts) error {
|
|
gui.Mutexes.PopupMutex.Lock()
|
|
defer gui.Mutexes.PopupMutex.Unlock()
|
|
|
|
ctx, cancel := context.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
|
|
if gui.State.CurrentPopupOpts != nil && !gui.State.CurrentPopupOpts.HasLoader {
|
|
gui.Log.Error("ignoring create popup panel because a popup panel is already open")
|
|
cancel()
|
|
return nil
|
|
}
|
|
|
|
// remove any previous keybindings
|
|
gui.clearConfirmationViewKeyBindings()
|
|
|
|
err := gui.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 := 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)
|
|
gui.resizeConfirmationPanel()
|
|
confirmationView.RenderTextArea()
|
|
} else {
|
|
if err := gui.renderString(confirmationView, style.AttrBold.Sprint(opts.Prompt)); err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := gui.setKeyBindings(cancel, opts); err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
|
|
gui.State.CurrentPopupOpts = &opts
|
|
|
|
return gui.c.PushContext(gui.State.Contexts.Confirmation)
|
|
}
|
|
|
|
func (gui *Gui) setKeyBindings(cancel context.CancelFunc, 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(cancel, opts.HandleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() })
|
|
} else {
|
|
onConfirm = gui.wrappedConfirmationFunction(cancel, opts.HandleConfirm)
|
|
}
|
|
|
|
keybindingConfig := gui.c.UserConfig.Keybinding
|
|
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
|
|
cancel,
|
|
opts.HandleConfirmPrompt,
|
|
gui.getSelectedSuggestionValue,
|
|
)
|
|
|
|
bindings := []*types.Binding{
|
|
{
|
|
ViewName: "confirmation",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.Confirm),
|
|
Handler: onConfirm,
|
|
},
|
|
{
|
|
ViewName: "confirmation",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1),
|
|
Handler: onConfirm,
|
|
},
|
|
{
|
|
ViewName: "confirmation",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.Return),
|
|
Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose),
|
|
},
|
|
{
|
|
ViewName: "confirmation",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.ReturnAlt1),
|
|
Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose),
|
|
},
|
|
{
|
|
ViewName: "confirmation",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel),
|
|
Handler: func() error {
|
|
if len(gui.State.Suggestions) > 0 {
|
|
return gui.replaceContext(gui.State.Contexts.Suggestions)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
ViewName: "suggestions",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.Confirm),
|
|
Handler: onSuggestionConfirm,
|
|
},
|
|
{
|
|
ViewName: "suggestions",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1),
|
|
Handler: onSuggestionConfirm,
|
|
},
|
|
{
|
|
ViewName: "suggestions",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.Return),
|
|
Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose),
|
|
},
|
|
{
|
|
ViewName: "suggestions",
|
|
Key: keybindings.GetKey(keybindingConfig.Universal.ReturnAlt1),
|
|
Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose),
|
|
},
|
|
{
|
|
ViewName: "suggestions",
|
|
Key: keybindings.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", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
|
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
|
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
|
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.ReturnAlt1), gocui.ModNone)
|
|
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
|
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
|
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
|
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.ReturnAlt1), gocui.ModNone)
|
|
}
|
|
|
|
func (gui *Gui) refreshSuggestions() {
|
|
gui.suggestionsAsyncHandler.Do(func() func() {
|
|
findSuggestionsFn := gui.findSuggestions
|
|
if findSuggestionsFn != nil {
|
|
suggestions := gui.findSuggestions(gui.c.GetPromptInput())
|
|
return func() { gui.setSuggestions(suggestions) }
|
|
} else {
|
|
return func() {}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (gui *Gui) handleAskFocused() error {
|
|
keybindingConfig := gui.c.UserConfig.Keybinding
|
|
|
|
message := utils.ResolvePlaceholderString(
|
|
gui.c.Tr.CloseConfirm,
|
|
map[string]string{
|
|
"keyBindClose": keybindings.Label(keybindingConfig.Universal.Return),
|
|
"keyBindConfirm": keybindings.Label(keybindingConfig.Universal.Confirm),
|
|
},
|
|
)
|
|
|
|
return gui.renderString(gui.Views.Options, message)
|
|
}
|