1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-10-08 22:52:12 +02:00

Make it possible to rebind the Confirm keybinding (#4860)

### PR Description

Remapping `keybinding.universal.confirm` from `<enter>` to something
like `y` is currently impossible because the same keybinding is also
used to confirm prompts (e.g. "New branch") and the search prompt. Fix
this by hard-coding enter for those; it doesn't really make sense to use
any other key for prompts.

While at it, add separate bindings for `confirmMenu` and
`confirmSuggestion` for those who would like to have different keys for
these. Of these, `confirmMenu` could be a little tricky because menus
are sometimes used purely as a choice (e.g. in "Amend commit attribute"
or the global keybindings menu), in which case you might want to use
`<enter>`, but other times as a substitute for a confirmation (e.g. for
"Delete branch"), in which case you might want to remap to `y`. I don't
have a great idea what to do about that, to be honest. Feedback welcome.

In this PR we only take care of Confirm, which many people seem to be
concerned about. We might consider doing something similar for Esc, but
it seems less urgent, and I'm out of time now. 😄

This seemingly simple change required some serious refactoring under the
hood, so thorough testing would be good to ensure we didn't break
anything.

Closes #2611
Closes #2767
Closes #3471

Related: #2768
This commit is contained in:
Stefan Haller
2025-09-05 10:46:33 +02:00
committed by GitHub
36 changed files with 365 additions and 156 deletions

View File

@@ -557,6 +557,8 @@ keybinding:
select: <space>
goInto: <enter>
confirm: <enter>
confirmMenu: <enter>
confirmSuggestion: <enter>
confirmInEditor: <a-enter>
confirmInEditor-alt: <c-s>
remove: d

View File

@@ -160,6 +160,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | Confirm | |
| `` <esc> `` | Close/Cancel | |
## Local branches
| Key | Action | Info |

View File

@@ -52,6 +52,13 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` ] `` | 次のタブ | |
| `` [ `` | 前のタブ | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | 確認 | |
| `` <esc> `` | 閉じる/キャンセル | |
## コミット
| Key | Action | Info |

View File

@@ -52,6 +52,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` ] `` | 이전 탭 | |
| `` [ `` | 다음 탭 | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | 확인 | |
| `` <esc> `` | 닫기/취소 | |
## Reflog
| Key | Action | Info |

View File

@@ -190,6 +190,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` w `` | View worktree options | |
| `` / `` | Start met zoeken | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | Bevestig | |
| `` <esc> `` | Sluiten | |
## Menu
| Key | Action | Info |

View File

@@ -125,6 +125,13 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` <esc> `` | Wyjdź z budowniczego niestandardowej łatki | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | Potwierdź | |
| `` <esc> `` | Zamknij/Anuluj | |
## Lokalne gałęzie
| Key | Action | Info |

View File

@@ -218,6 +218,13 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | Confirmar | |
| `` <esc> `` | Fechar/Cancelar | |
## Menu
| Key | Action | Info |

View File

@@ -52,6 +52,13 @@ _Связки клавиш_
| `` ] `` | Следующая вкладка | |
| `` [ `` | Предыдущая вкладка | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | Подтвердить | |
| `` <esc> `` | Закрыть/отменить | |
## Worktrees
| Key | Action | Info |

View File

@@ -52,6 +52,13 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` ] `` | 下一个标签 | |
| `` [ `` | 上一个标签 | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | 确认 | |
| `` <esc> `` | 关闭 | |
## Reflog
| Key | Action | Info |

View File

@@ -52,6 +52,13 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` ] `` | 下一個索引標籤 | |
| `` [ `` | 上一個索引標籤 | |
## Input prompt
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | 確認 | |
| `` <esc> `` | 關閉/取消 | |
## 主面板 (補丁生成)
| Key | Action | Info |

View File

@@ -116,6 +116,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
"commitDescription": tr.CommitDescriptionTitle,
"commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle,
"prompt": tr.PromptTitle,
"information": tr.InformationTitle,
"main": tr.NormalTitle,
"patchBuilding": tr.PatchBuildingTitle,

View File

@@ -425,6 +425,8 @@ type KeybindingUniversalConfig struct {
Select string `yaml:"select"`
GoInto string `yaml:"goInto"`
Confirm string `yaml:"confirm"`
ConfirmMenu string `yaml:"confirmMenu"`
ConfirmSuggestion string `yaml:"confirmSuggestion"`
ConfirmInEditor string `yaml:"confirmInEditor"`
ConfirmInEditorAlt string `yaml:"confirmInEditor-alt"`
Remove string `yaml:"remove"`
@@ -889,6 +891,8 @@ func GetDefaultConfig() *UserConfig {
Select: "<space>",
GoInto: "<enter>",
Confirm: "<enter>",
ConfirmMenu: "<enter>",
ConfirmSuggestion: "<enter>",
ConfirmInEditor: "<a-enter>",
ConfirmInEditorAlt: "<c-s>",
Remove: "d",

View File

@@ -41,6 +41,7 @@ const (
MENU_CONTEXT_KEY types.ContextKey = "menu"
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
PROMPT_CONTEXT_KEY types.ContextKey = "prompt"
SEARCH_CONTEXT_KEY types.ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage"
COMMIT_DESCRIPTION_CONTEXT_KEY types.ContextKey = "commitDescription"
@@ -73,6 +74,7 @@ var AllContextKeys = []types.ContextKey{
MENU_CONTEXT_KEY,
CONFIRMATION_CONTEXT_KEY,
PROMPT_CONTEXT_KEY,
SEARCH_CONTEXT_KEY,
COMMIT_MESSAGE_CONTEXT_KEY,
SUBMODULES_CONTEXT_KEY,
@@ -106,6 +108,7 @@ type ContextTree struct {
CustomPatchBuilderSecondary types.Context
MergeConflicts *MergeConflictsContext
Confirmation *ConfirmationContext
Prompt *PromptContext
CommitMessage *CommitMessageContext
CommitDescription types.Context
CommandLog types.Context
@@ -141,6 +144,7 @@ func (self *ContextTree) Flatten() []types.Context {
self.Stash,
self.Menu,
self.Confirmation,
self.Prompt,
self.CommitMessage,
self.CommitDescription,

View File

@@ -0,0 +1,30 @@
package context
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type PromptContext struct {
*SimpleContext
c *ContextCommon
State ConfirmationContextState
}
var _ types.Context = (*PromptContext)(nil)
func NewPromptContext(
c *ContextCommon,
) *PromptContext {
return &PromptContext{
c: c,
SimpleContext: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Prompt,
WindowName: "prompt",
Key: PROMPT_CONTEXT_KEY,
Kind: types.TEMPORARY_POPUP,
Focusable: true,
HasUncontrolledBounds: true,
})),
}
}

View File

@@ -84,6 +84,7 @@ func NewContextTree(c *ContextCommon) *ContextTree {
c,
),
Confirmation: NewConfirmationContext(c),
Prompt: NewPromptContext(c),
CommitMessage: NewCommitMessageContext(c),
CommitDescription: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{

View File

@@ -193,6 +193,7 @@ func (gui *Gui) resetHelpersAndControllers() {
statusController := controllers.NewStatusController(common)
commandLogController := controllers.NewCommandLogController(common)
confirmationController := controllers.NewConfirmationController(common)
promptController := controllers.NewPromptController(common)
suggestionsController := controllers.NewSuggestionsController(common)
jumpToSideWindowController := controllers.NewJumpToSideWindowController(common, gui.handleNextTab)
@@ -399,6 +400,10 @@ func (gui *Gui) resetHelpersAndControllers() {
confirmationController,
)
controllers.AttachControllers(gui.State.Contexts.Prompt,
promptController,
)
controllers.AttachControllers(gui.State.Contexts.Suggestions,
suggestionsController,
)

View File

@@ -1,9 +1,6 @@
package controllers
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@@ -39,45 +36,19 @@ func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) [
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error {
if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 {
self.switchToSuggestions()
}
return nil
},
},
{
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopyToClipboard,
Description: self.c.Tr.CopyToClipboardMenu,
DisplayOnScreen: true,
GetDisabledReason: self.copyToClipboardEnabled,
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.handleCopyToClipboard,
Description: self.c.Tr.CopyToClipboardMenu,
DisplayOnScreen: true,
},
}
return bindings
}
func (self *ConfirmationController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: self.c.Contexts().Suggestions.GetViewName(),
FocusedView: self.c.Contexts().Confirmation.GetViewName(),
Key: gocui.MouseLeft,
Handler: func(gocui.ViewMouseBindingOpts) error {
self.switchToSuggestions()
// Let it fall through to the ListController's click handler so that
// the clicked line gets selected:
return gocui.ErrKeybindingNotHandled
},
},
}
}
func (self *ConfirmationController) GetOnFocusLost() func(types.OnFocusLostOpts) {
return func(types.OnFocusLostOpts) {
self.c.Helpers().Confirmation.DeactivateConfirmationPrompt()
self.c.Helpers().Confirmation.DeactivateConfirmation()
}
}
@@ -89,18 +60,6 @@ func (self *ConfirmationController) context() *context.ConfirmationContext {
return self.c.Contexts().Confirmation
}
func (self *ConfirmationController) switchToSuggestions() {
subtitle := ""
if self.c.State().GetRepoState().GetCurrentPopupOpts().HandleDeleteSuggestion != nil {
// We assume that whenever things are deletable, they
// are also editable, so we show both keybindings
subtitle = fmt.Sprintf(self.c.Tr.SuggestionsSubtitle,
self.c.UserConfig().Keybinding.Universal.Remove, self.c.UserConfig().Keybinding.Universal.Edit)
}
self.c.Views().Suggestions.Subtitle = subtitle
self.c.Context().Replace(self.c.Contexts().Suggestions)
}
func (self *ConfirmationController) handleCopyToClipboard() error {
confirmationView := self.c.Views().Confirmation
text := confirmationView.Buffer()
@@ -111,12 +70,3 @@ func (self *ConfirmationController) handleCopyToClipboard() error {
self.c.Toast(self.c.Tr.MessageCopiedToClipboard)
return nil
}
func (self *ConfirmationController) copyToClipboardEnabled() *types.DisabledReason {
if self.c.Views().Confirmation.Editable {
// The empty text is intentional. We don't want to get a toast when invoking this, we only
// want to prevent it from showing up in the options bar.
return &types.DisabledReason{Text: ""}
}
return nil
}

View File

@@ -46,17 +46,27 @@ func (self *ConfirmationHelper) wrappedPromptConfirmationFunction(cancel goConte
})
}
func (self *ConfirmationHelper) DeactivateConfirmationPrompt() {
func (self *ConfirmationHelper) DeactivateConfirmation() {
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 (self *ConfirmationHelper) DeactivatePrompt() {
self.c.Mutexes().PopupMutex.Lock()
self.c.State().GetRepoState().SetCurrentPopupOpts(nil)
self.c.Mutexes().PopupMutex.Unlock()
self.c.Views().Prompt.Visible = false
self.c.Views().Suggestions.Visible = false
self.clearPromptViewKeyBindings()
}
func getMessageHeight(wrap bool, editable bool, message string, width int, tabWidth int) int {
wrappedLines, _, _ := utils.WrapViewLinesToWidth(wrap, editable, message, width, tabWidth)
return len(wrappedLines)
@@ -105,15 +115,28 @@ func (self *ConfirmationHelper) prepareConfirmationPanel(
opts types.ConfirmOpts,
) {
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
self.c.ResetViewOrigin(self.c.Views().Confirmation)
self.c.SetViewContent(self.c.Views().Confirmation, style.AttrBold.Sprint(strings.TrimSpace(opts.Prompt)))
}
func (self *ConfirmationHelper) preparePromptPanel(
opts types.ConfirmOpts,
) {
self.c.Views().Prompt.Title = opts.Title
self.c.Views().Prompt.FgColor = theme.GocuiDefaultTextColor
self.c.Views().Prompt.Mask = runeForMask(opts.Mask)
self.c.Views().Prompt.SetOrigin(0, 0)
textArea := self.c.Views().Prompt.TextArea
textArea.Clear()
textArea.TypeString(opts.Prompt)
self.c.Views().Prompt.RenderTextArea()
if opts.FindSuggestionsFunc != nil {
suggestionsContext := self.c.Contexts().Suggestions
suggestionsContext.State.FindSuggestions = opts.FindSuggestionsFunc
suggestionsView := self.c.Views().Suggestions
suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor
@@ -150,44 +173,59 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
// remove any previous keybindings
self.clearConfirmationViewKeyBindings()
self.clearPromptViewKeyBindings()
self.prepareConfirmationPanel(
types.ConfirmOpts{
Title: opts.Title,
Prompt: opts.Prompt,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Editable: opts.Editable,
Mask: opts.Mask,
})
confirmationView := self.c.Views().Confirmation
confirmationView.Editable = opts.Editable
var context types.Context
if opts.Editable {
textArea := confirmationView.TextArea
textArea.Clear()
textArea.TypeString(opts.Prompt)
confirmationView.RenderTextArea()
} else {
self.c.ResetViewOrigin(confirmationView)
self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(strings.TrimSpace(opts.Prompt)))
}
self.c.Contexts().Suggestions.State.FindSuggestions = opts.FindSuggestionsFunc
self.setKeyBindings(cancel, opts)
self.preparePromptPanel(
types.ConfirmOpts{
Title: opts.Title,
Prompt: opts.Prompt,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Mask: opts.Mask,
})
context = self.c.Contexts().Prompt
self.setPromptKeyBindings(cancel, opts)
} else {
if opts.FindSuggestionsFunc != nil {
panic("non-editable confirmation views do not support suggestions")
}
self.c.Contexts().Suggestions.State.FindSuggestions = nil
self.prepareConfirmationPanel(
types.ConfirmOpts{
Title: opts.Title,
Prompt: opts.Prompt,
})
context = self.c.Contexts().Confirmation
self.setConfirmationKeyBindings(cancel, opts)
}
self.c.Contexts().Suggestions.State.AllowEditSuggestion = opts.AllowEditSuggestion
self.c.State().GetRepoState().SetCurrentPopupOpts(&opts)
self.c.Context().Push(self.c.Contexts().Confirmation, types.OnFocusOpts{})
self.c.Context().Push(context, types.OnFocusOpts{})
}
func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) {
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)
}
func (self *ConfirmationHelper) setConfirmationKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) {
onConfirm := self.wrappedConfirmationFunction(cancel, opts.HandleConfirm)
onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose)
self.c.Contexts().Confirmation.State.OnConfirm = onConfirm
self.c.Contexts().Confirmation.State.OnClose = onClose
}
func (self *ConfirmationHelper) setPromptKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) {
onConfirm := self.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt,
func() string { return self.c.Views().Prompt.TextArea.GetContent() })
onSuggestionConfirm := self.wrappedPromptConfirmationFunction(
cancel,
@@ -206,8 +244,8 @@ func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts
return opts.HandleDeleteSuggestion(idx)
}
self.c.Contexts().Confirmation.State.OnConfirm = onConfirm
self.c.Contexts().Confirmation.State.OnClose = onClose
self.c.Contexts().Prompt.State.OnConfirm = onConfirm
self.c.Contexts().Prompt.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm
self.c.Contexts().Suggestions.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = onDeleteSuggestion
@@ -217,6 +255,12 @@ func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() {
noop := func() error { return nil }
self.c.Contexts().Confirmation.State.OnConfirm = noop
self.c.Contexts().Confirmation.State.OnClose = noop
}
func (self *ConfirmationHelper) clearPromptViewKeyBindings() {
noop := func() error { return nil }
self.c.Contexts().Prompt.State.OnConfirm = noop
self.c.Contexts().Prompt.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnConfirm = noop
self.c.Contexts().Suggestions.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = noop
@@ -238,8 +282,10 @@ func (self *ConfirmationHelper) ResizeCurrentPopupPanels() {
switch c {
case self.c.Contexts().Menu:
self.resizeMenu(parentPopupContext)
case self.c.Contexts().Confirmation, self.c.Contexts().Suggestions:
case self.c.Contexts().Confirmation:
self.resizeConfirmationPanel(parentPopupContext)
case self.c.Contexts().Prompt, self.c.Contexts().Suggestions:
self.resizePromptPanel(parentPopupContext)
case self.c.Contexts().CommitMessage, self.c.Contexts().CommitDescription:
self.ResizeCommitMessagePanels(parentPopupContext)
}
@@ -300,26 +346,30 @@ func (self *ConfirmationHelper) layoutMenuPrompt(contentWidth int) int {
}
func (self *ConfirmationHelper) resizeConfirmationPanel(parentPopupContext types.Context) {
panelWidth := self.getPopupPanelWidth()
contentWidth := panelWidth - 2 // minus 2 for the frame
confirmationView := self.c.Views().Confirmation
prompt := confirmationView.Buffer()
panelHeight := getMessageHeight(true, false, prompt, contentWidth, confirmationView.TabWidth)
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight, parentPopupContext)
_, _ = self.c.GocuiGui().SetView(confirmationView.Name(), x0, y0, x1, y1, 0)
}
func (self *ConfirmationHelper) resizePromptPanel(parentPopupContext types.Context) {
suggestionsViewHeight := 0
if self.c.Views().Suggestions.Visible {
suggestionsViewHeight = 11
}
panelWidth := self.getPopupPanelWidth()
contentWidth := panelWidth - 2 // minus 2 for the frame
confirmationView := self.c.Views().Confirmation
prompt := confirmationView.Buffer()
wrap := true
editable := confirmationView.Editable
if editable {
prompt = confirmationView.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, editable, prompt, contentWidth, confirmationView.TabWidth) + suggestionsViewHeight
promptView := self.c.Views().Prompt
prompt := promptView.TextArea.GetContent()
panelHeight := getMessageHeight(false, true, prompt, contentWidth, promptView.TabWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight, parentPopupContext)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(confirmationView.Name(), x0, y0, x1, confirmationViewBottom, 0)
promptViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(promptView.Name(), x0, y0, x1, promptViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
suggestionsViewTop := promptViewBottom + 1
_, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
}

View File

@@ -38,7 +38,7 @@ func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types.
GetDisabledReason: self.require(self.singleItemSelected()),
},
{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Key: opts.GetKey(opts.Config.Universal.ConfirmMenu),
Handler: self.withItem(self.press),
GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.Execute,

View File

@@ -0,0 +1,95 @@
package controllers
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type PromptController struct {
baseController
c *ControllerCommon
}
var _ types.IController = &PromptController{}
func NewPromptController(
c *ControllerCommon,
) *PromptController {
return &PromptController{
baseController: baseController{},
c: c,
}
}
func (self *PromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: gocui.KeyEnter,
Handler: func() error { return self.context().State.OnConfirm() },
Description: self.c.Tr.Confirm,
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: func() error { return self.context().State.OnClose() },
Description: self.c.Tr.CloseCancel,
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error {
if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 {
self.switchToSuggestions()
}
return nil
},
},
}
return bindings
}
func (self *PromptController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: self.c.Contexts().Suggestions.GetViewName(),
FocusedView: self.c.Contexts().Prompt.GetViewName(),
Key: gocui.MouseLeft,
Handler: func(gocui.ViewMouseBindingOpts) error {
self.switchToSuggestions()
// Let it fall through to the ListController's click handler so that
// the clicked line gets selected:
return gocui.ErrKeybindingNotHandled
},
},
}
}
func (self *PromptController) GetOnFocusLost() func(types.OnFocusLostOpts) {
return func(types.OnFocusLostOpts) {
self.c.Helpers().Confirmation.DeactivatePrompt()
}
}
func (self *PromptController) Context() types.Context {
return self.context()
}
func (self *PromptController) context() *context.PromptContext {
return self.c.Contexts().Prompt
}
func (self *PromptController) switchToSuggestions() {
subtitle := ""
if self.c.State().GetRepoState().GetCurrentPopupOpts().HandleDeleteSuggestion != nil {
// We assume that whenever things are deletable, they
// are also editable, so we show both keybindings
subtitle = fmt.Sprintf(self.c.Tr.SuggestionsSubtitle,
self.c.UserConfig().Keybinding.Universal.Remove, self.c.UserConfig().Keybinding.Universal.Edit)
}
self.c.Views().Suggestions.Subtitle = subtitle
self.c.Context().Replace(self.c.Contexts().Suggestions)
}

View File

@@ -24,7 +24,7 @@ func NewSearchPromptController(
func (self *SearchPromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: self.confirm,
},

View File

@@ -32,7 +32,7 @@ func NewSuggestionsController(
func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Key: opts.GetKey(opts.Config.Universal.ConfirmSuggestion),
Handler: func() error { return self.context().State.OnConfirm() },
GetDisabledReason: self.require(self.singleItemSelected()),
},
@@ -42,7 +42,7 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: self.switchToConfirmation,
Handler: self.switchToPrompt,
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
@@ -55,11 +55,11 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []
Handler: func() error {
if self.context().State.AllowEditSuggestion {
if selectedItem := self.c.Contexts().Suggestions.GetSelected(); selectedItem != nil {
self.c.Contexts().Confirmation.GetView().TextArea.Clear()
self.c.Contexts().Confirmation.GetView().TextArea.TypeString(selectedItem.Value)
self.c.Contexts().Confirmation.GetView().RenderTextArea()
self.c.Contexts().Prompt.GetView().TextArea.Clear()
self.c.Contexts().Prompt.GetView().TextArea.TypeString(selectedItem.Value)
self.c.Contexts().Prompt.GetView().RenderTextArea()
self.c.Contexts().Suggestions.RefreshSuggestions()
return self.switchToConfirmation()
return self.switchToPrompt()
}
}
return nil
@@ -73,26 +73,26 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []
func (self *SuggestionsController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: self.c.Contexts().Confirmation.GetViewName(),
ViewName: self.c.Contexts().Prompt.GetViewName(),
FocusedView: self.c.Contexts().Suggestions.GetViewName(),
Key: gocui.MouseLeft,
Handler: func(gocui.ViewMouseBindingOpts) error {
return self.switchToConfirmation()
return self.switchToPrompt()
},
},
}
}
func (self *SuggestionsController) switchToConfirmation() error {
func (self *SuggestionsController) switchToPrompt() error {
self.c.Views().Suggestions.Subtitle = ""
self.c.Views().Suggestions.Highlight = false
self.c.Context().Replace(self.c.Contexts().Confirmation)
self.c.Context().Replace(self.c.Contexts().Prompt)
return nil
}
func (self *SuggestionsController) GetOnFocusLost() func(types.OnFocusLostOpts) {
return func(types.OnFocusLostOpts) {
self.c.Helpers().Confirmation.DeactivateConfirmationPrompt()
self.c.Helpers().Confirmation.DeactivatePrompt()
}
}

View File

@@ -52,7 +52,7 @@ func (self *WorktreesController) GetKeybindings(opts types.KeybindingsOpts) []*t
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.withItem(self.enter),
GetDisabledReason: self.require(self.singleItemSelected()),
},

View File

@@ -90,60 +90,36 @@ func (gui *Gui) scrollDownSecondary() error {
}
func (gui *Gui) scrollUpConfirmationPanel() error {
if gui.Views.Confirmation.Editable {
return nil
}
gui.scrollUpView(gui.Views.Confirmation)
return nil
}
func (gui *Gui) scrollDownConfirmationPanel() error {
if gui.Views.Confirmation.Editable {
return nil
}
gui.scrollDownView(gui.Views.Confirmation)
return nil
}
func (gui *Gui) pageUpConfirmationPanel() error {
if gui.Views.Confirmation.Editable {
return nil
}
gui.Views.Confirmation.ScrollUp(gui.Contexts().Confirmation.GetViewTrait().PageDelta())
return nil
}
func (gui *Gui) pageDownConfirmationPanel() error {
if gui.Views.Confirmation.Editable {
return nil
}
gui.Views.Confirmation.ScrollDown(gui.Contexts().Confirmation.GetViewTrait().PageDelta())
return nil
}
func (gui *Gui) goToConfirmationPanelTop() error {
if gui.Views.Confirmation.Editable {
return gocui.ErrKeybindingNotHandled
}
gui.Views.Confirmation.ScrollUp(gui.Views.Confirmation.ViewLinesHeight())
return nil
}
func (gui *Gui) goToConfirmationPanelBottom() error {
if gui.Views.Confirmation.Editable {
return gocui.ErrKeybindingNotHandled
}
gui.Views.Confirmation.ScrollDown(gui.Views.Confirmation.ViewLinesHeight())
return nil

View File

@@ -697,7 +697,7 @@ func NewGui(
return gui.helpers.AppStatus.WithWaitingStatusSync(message, f)
},
func(message string, kind types.ToastKind) { gui.helpers.AppStatus.Toast(message, kind) },
func() string { return gui.Views.Confirmation.TextArea.GetContent() },
func() string { return gui.Views.Prompt.TextArea.GetContent() },
func() bool { return gui.c.InDemo() },
)

View File

@@ -507,11 +507,11 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error {
!gocui.IsMouseScrollKey(opts.Key) {
// we ignore click events on views that aren't popup panels, when a popup panel is focused.
// Unless both the current view and the clicked-on view are either commit message or commit
// description, or a confirmation and the suggestions view, because we want to allow switching
// description, or a prompt and the suggestions view, because we want to allow switching
// between those two views by clicking.
isCommitMessageOrSuggestionsView := func(viewName string) bool {
return viewName == "commitMessage" || viewName == "commitDescription" ||
viewName == "confirmation" || viewName == "suggestions"
viewName == "prompt" || viewName == "suggestions"
}
if !isCommitMessageOrSuggestionsView(gui.currentViewName()) || !isCommitMessageOrSuggestionsView(binding.ViewName) {
return nil

View File

@@ -3,6 +3,7 @@ package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
)
@@ -20,6 +21,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
}
maxColumnSize := 1
confirmKey := keybindings.GetKey(gui.c.UserConfig().Keybinding.Universal.ConfirmMenu)
for _, item := range opts.Items {
if item.LabelColumns == nil {
@@ -31,6 +33,11 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
}
maxColumnSize = max(maxColumnSize, len(item.LabelColumns))
// Remove all item keybindings that are the same as the confirm binding
if item.Key == confirmKey {
item.Key = nil
}
}
for _, item := range opts.Items {

View File

@@ -25,6 +25,7 @@ type Views struct {
Options *gocui.View
Confirmation *gocui.View
Prompt *gocui.View
Menu *gocui.View
CommitMessage *gocui.View
CommitDescription *gocui.View

View File

@@ -68,6 +68,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
{viewPtr: &gui.Views.Menu, name: "menu"},
{viewPtr: &gui.Views.Suggestions, name: "suggestions"},
{viewPtr: &gui.Views.Confirmation, name: "confirmation"},
{viewPtr: &gui.Views.Prompt, name: "prompt"},
{viewPtr: &gui.Views.Tooltip, name: "tooltip"},
// this guy will cover everything else when it appears
@@ -127,9 +128,14 @@ func (gui *Gui) createAllViews() error {
gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor)
gui.Views.Confirmation.Visible = false
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.promptEditor)
gui.Views.Confirmation.Wrap = true
gui.Views.Confirmation.AutoRenderHyperLinks = true
gui.Views.Prompt.Visible = false
gui.Views.Prompt.Wrap = false // We don't want wrapping in one-line prompts
gui.Views.Prompt.Editable = true
gui.Views.Prompt.Editor = gocui.EditorFunc(gui.promptEditor)
gui.Views.Suggestions.Visible = false
gui.Views.Menu.Visible = false

View File

@@ -611,6 +611,7 @@ type TranslationSet struct {
MustStashWarning string
MustStashTitle string
ConfirmationTitle string
PromptTitle string
PrevPage string
NextPage string
GotoTop string
@@ -1692,6 +1693,7 @@ func EnglishTranslationSet() *TranslationSet {
MustStashWarning: "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
MustStashTitle: "Must stash",
ConfirmationTitle: "Confirmation panel",
PromptTitle: "Input prompt",
PrevPage: "Previous page",
NextPage: "Next page",
GotoTop: "Scroll to top",

View File

@@ -27,7 +27,7 @@ func (self *CommitDescriptionPanelDriver) SwitchToSummary() *CommitMessagePanelD
}
func (self *CommitDescriptionPanelDriver) AddNewline() *CommitDescriptionPanelDriver {
self.t.pressFast(self.t.keys.Universal.Confirm)
self.t.pressFast("<enter>")
return self
}

View File

@@ -21,7 +21,7 @@ func (self *MenuDriver) Title(expected *TextMatcher) *MenuDriver {
func (self *MenuDriver) Confirm() *MenuDriver {
self.checkNecessaryChecksCompleted()
self.getViewDriver().PressEnter()
self.getViewDriver().Press(self.t.keys.Universal.ConfirmMenu)
return self
}

View File

@@ -13,7 +13,7 @@ func (self *Popup) Confirmation() *ConfirmationDriver {
func (self *Popup) inConfirm() {
self.t.assertWithRetries(func() (bool, string) {
currentView := self.t.gui.CurrentContext().GetView()
return currentView.Name() == "confirmation" && !currentView.Editable, "Expected confirmation popup to be focused"
return currentView.Name() == "confirmation", "Expected confirmation popup to be focused"
})
}
@@ -26,7 +26,7 @@ func (self *Popup) Prompt() *PromptDriver {
func (self *Popup) inPrompt() {
self.t.assertWithRetries(func() (bool, string) {
currentView := self.t.gui.CurrentContext().GetView()
return currentView.Name() == "confirmation" && currentView.Editable, "Expected prompt popup to be focused"
return currentView.Name() == "prompt", "Expected prompt popup to be focused"
})
}
@@ -45,7 +45,7 @@ func (self *Popup) inAlert() {
// basically the same thing as a confirmation popup with the current implementation
self.t.assertWithRetries(func() (bool, string) {
currentView := self.t.gui.CurrentContext().GetView()
return currentView.Name() == "confirmation" && !currentView.Editable, "Expected alert popup to be focused"
return currentView.Name() == "confirmation", "Expected alert popup to be focused"
})
}

View File

@@ -6,7 +6,7 @@ type PromptDriver struct {
}
func (self *PromptDriver) getViewDriver() *ViewDriver {
return self.t.Views().Confirmation()
return self.t.Views().Prompt()
}
// asserts that the popup has the expected title
@@ -72,7 +72,7 @@ func (self *PromptDriver) ConfirmFirstSuggestion() {
self.t.Views().Suggestions().
IsFocused().
SelectedLineIdx(0).
PressEnter()
Press(self.t.keys.Universal.ConfirmSuggestion)
}
func (self *PromptDriver) ConfirmSuggestion(matcher *TextMatcher) {
@@ -80,7 +80,7 @@ func (self *PromptDriver) ConfirmSuggestion(matcher *TextMatcher) {
self.t.Views().Suggestions().
IsFocused().
NavigateToLine(matcher).
PressEnter()
Press(self.t.keys.Universal.ConfirmSuggestion)
}
func (self *PromptDriver) DeleteSuggestion(matcher *TextMatcher) *PromptDriver {

View File

@@ -128,6 +128,10 @@ func (self *Views) Confirmation() *ViewDriver {
return self.regularView("confirmation")
}
func (self *Views) Prompt() *ViewDriver {
return self.regularView("prompt")
}
func (self *Views) CommitMessage() *ViewDriver {
return self.regularView("commitMessage")
}

View File

@@ -1366,6 +1366,14 @@
"type": "string",
"default": "\u003center\u003e"
},
"confirmMenu": {
"type": "string",
"default": "\u003center\u003e"
},
"confirmSuggestion": {
"type": "string",
"default": "\u003center\u003e"
},
"confirmInEditor": {
"type": "string",
"default": "\u003ca-enter\u003e"