Convert JumpToBlock to a list of multi-key bindings

JumpToBlock is special: each of its 5 elements is the binding for one side
window (status / files / branches / commits / stash), not an alternate for a
single command. Change the field from []string to []Keybinding so each window
slot can have alternates of its own.

The schema becomes "an array of 5 keybindings, each itself a string or array of
strings", which falls out cleanly from how the Keybinding type inlines into the
generated schema. Existing configs (a flat array of 5 strings) keep validating
because each element is unmarshalled through Keybinding's scalar-or-sequence
decoder.
This commit is contained in:
Stefan Haller
2026-05-25 15:32:47 +02:00
parent 5748d82073
commit 3ecca88bd8
9 changed files with 127 additions and 98 deletions
+15
View File
@@ -180,3 +180,18 @@ func TestKeybindingConfigYAMLAcceptsBothForms(t *testing.T) {
})
}
}
func TestJumpToBlockYAMLAcceptsMixedForms(t *testing.T) {
yamlInput := `
jumpToBlock:
- "1"
- ["2", "@"]
- "3"
- "4"
- "5"
`
var cfg KeybindingUniversalConfig
assert.NoError(t, yaml.Unmarshal([]byte(yamlInput), &cfg))
expected := []Keybinding{{"1"}, {"2", "@"}, {"3"}, {"4"}, {"5"}}
assert.Equal(t, expected, cfg.JumpToBlock)
}
+81 -81
View File
@@ -423,86 +423,86 @@ type KeybindingConfig struct {
// damn looks like we have some inconsistencies here with -alt and -alt1
type KeybindingUniversalConfig struct {
Quit Keybinding `yaml:"quit"`
QuitAlt1 Keybinding `yaml:"quit-alt1"`
SuspendApp Keybinding `yaml:"suspendApp"`
Return Keybinding `yaml:"return"`
QuitWithoutChangingDirectory Keybinding `yaml:"quitWithoutChangingDirectory"`
TogglePanel Keybinding `yaml:"togglePanel"`
PrevItem Keybinding `yaml:"prevItem"`
NextItem Keybinding `yaml:"nextItem"`
PrevItemAlt Keybinding `yaml:"prevItem-alt"`
NextItemAlt Keybinding `yaml:"nextItem-alt"`
PrevPage Keybinding `yaml:"prevPage"`
NextPage Keybinding `yaml:"nextPage"`
ScrollLeft Keybinding `yaml:"scrollLeft"`
ScrollRight Keybinding `yaml:"scrollRight"`
GotoTop Keybinding `yaml:"gotoTop"`
GotoBottom Keybinding `yaml:"gotoBottom"`
GotoTopAlt Keybinding `yaml:"gotoTop-alt"`
GotoBottomAlt Keybinding `yaml:"gotoBottom-alt"`
ToggleRangeSelect Keybinding `yaml:"toggleRangeSelect"`
RangeSelectDown Keybinding `yaml:"rangeSelectDown"`
RangeSelectUp Keybinding `yaml:"rangeSelectUp"`
PrevBlock Keybinding `yaml:"prevBlock"`
NextBlock Keybinding `yaml:"nextBlock"`
PrevBlockAlt Keybinding `yaml:"prevBlock-alt"`
NextBlockAlt Keybinding `yaml:"nextBlock-alt"`
NextBlockAlt2 Keybinding `yaml:"nextBlock-alt2"`
PrevBlockAlt2 Keybinding `yaml:"prevBlock-alt2"`
JumpToBlock []string `yaml:"jumpToBlock"`
FocusMainView Keybinding `yaml:"focusMainView"`
NextMatch Keybinding `yaml:"nextMatch"`
PrevMatch Keybinding `yaml:"prevMatch"`
StartSearch Keybinding `yaml:"startSearch"`
MoveWordLeft Keybinding `yaml:"moveWordLeft"` // <alt+left> on Mac
MoveWordRight Keybinding `yaml:"moveWordRight"` // <alt+right> on Mac
BackspaceWord Keybinding `yaml:"backspaceWord"` // <alt+backspace> on Mac
ForwardDeleteWord Keybinding `yaml:"forwardDeleteWord"` // <alt+delete> on Mac
OptionMenu Keybinding `yaml:"optionMenu"`
Select Keybinding `yaml:"select"`
GoInto Keybinding `yaml:"goInto"`
Confirm Keybinding `yaml:"confirm"`
ConfirmMenu Keybinding `yaml:"confirmMenu"`
ConfirmSuggestion Keybinding `yaml:"confirmSuggestion"`
ConfirmInEditor Keybinding `yaml:"confirmInEditor"` // <meta+enter> on Mac
ConfirmInEditorAlt Keybinding `yaml:"confirmInEditor-alt"`
Remove Keybinding `yaml:"remove"`
New Keybinding `yaml:"new"`
Edit Keybinding `yaml:"edit"`
OpenFile Keybinding `yaml:"openFile"`
ScrollUpMain Keybinding `yaml:"scrollUpMain"`
ScrollDownMain Keybinding `yaml:"scrollDownMain"`
ScrollUpMainAlt1 Keybinding `yaml:"scrollUpMain-alt1"`
ScrollDownMainAlt1 Keybinding `yaml:"scrollDownMain-alt1"`
ScrollUpMainAlt2 Keybinding `yaml:"scrollUpMain-alt2"`
ScrollDownMainAlt2 Keybinding `yaml:"scrollDownMain-alt2"`
ExecuteShellCommand Keybinding `yaml:"executeShellCommand"`
CreateRebaseOptionsMenu Keybinding `yaml:"createRebaseOptionsMenu"`
Push Keybinding `yaml:"pushFiles"` // 'Files' appended for legacy reasons
Pull Keybinding `yaml:"pullFiles"` // 'Files' appended for legacy reasons
Refresh Keybinding `yaml:"refresh"`
CreatePatchOptionsMenu Keybinding `yaml:"createPatchOptionsMenu"`
NextTab Keybinding `yaml:"nextTab"`
PrevTab Keybinding `yaml:"prevTab"`
NextScreenMode Keybinding `yaml:"nextScreenMode"`
PrevScreenMode Keybinding `yaml:"prevScreenMode"`
CyclePagers Keybinding `yaml:"cyclePagers"`
Undo Keybinding `yaml:"undo"`
Redo Keybinding `yaml:"redo"`
FilteringMenu Keybinding `yaml:"filteringMenu"`
DiffingMenu Keybinding `yaml:"diffingMenu"`
DiffingMenuAlt Keybinding `yaml:"diffingMenu-alt"`
CopyToClipboard Keybinding `yaml:"copyToClipboard"`
OpenRecentRepos Keybinding `yaml:"openRecentRepos"`
SubmitEditorText Keybinding `yaml:"submitEditorText"`
ExtrasMenu Keybinding `yaml:"extrasMenu"`
ToggleWhitespaceInDiffView Keybinding `yaml:"toggleWhitespaceInDiffView"`
IncreaseContextInDiffView Keybinding `yaml:"increaseContextInDiffView"`
DecreaseContextInDiffView Keybinding `yaml:"decreaseContextInDiffView"`
IncreaseRenameSimilarityThreshold Keybinding `yaml:"increaseRenameSimilarityThreshold"`
DecreaseRenameSimilarityThreshold Keybinding `yaml:"decreaseRenameSimilarityThreshold"`
OpenDiffTool Keybinding `yaml:"openDiffTool"`
Quit Keybinding `yaml:"quit"`
QuitAlt1 Keybinding `yaml:"quit-alt1"`
SuspendApp Keybinding `yaml:"suspendApp"`
Return Keybinding `yaml:"return"`
QuitWithoutChangingDirectory Keybinding `yaml:"quitWithoutChangingDirectory"`
TogglePanel Keybinding `yaml:"togglePanel"`
PrevItem Keybinding `yaml:"prevItem"`
NextItem Keybinding `yaml:"nextItem"`
PrevItemAlt Keybinding `yaml:"prevItem-alt"`
NextItemAlt Keybinding `yaml:"nextItem-alt"`
PrevPage Keybinding `yaml:"prevPage"`
NextPage Keybinding `yaml:"nextPage"`
ScrollLeft Keybinding `yaml:"scrollLeft"`
ScrollRight Keybinding `yaml:"scrollRight"`
GotoTop Keybinding `yaml:"gotoTop"`
GotoBottom Keybinding `yaml:"gotoBottom"`
GotoTopAlt Keybinding `yaml:"gotoTop-alt"`
GotoBottomAlt Keybinding `yaml:"gotoBottom-alt"`
ToggleRangeSelect Keybinding `yaml:"toggleRangeSelect"`
RangeSelectDown Keybinding `yaml:"rangeSelectDown"`
RangeSelectUp Keybinding `yaml:"rangeSelectUp"`
PrevBlock Keybinding `yaml:"prevBlock"`
NextBlock Keybinding `yaml:"nextBlock"`
PrevBlockAlt Keybinding `yaml:"prevBlock-alt"`
NextBlockAlt Keybinding `yaml:"nextBlock-alt"`
NextBlockAlt2 Keybinding `yaml:"nextBlock-alt2"`
PrevBlockAlt2 Keybinding `yaml:"prevBlock-alt2"`
JumpToBlock []Keybinding `yaml:"jumpToBlock"`
FocusMainView Keybinding `yaml:"focusMainView"`
NextMatch Keybinding `yaml:"nextMatch"`
PrevMatch Keybinding `yaml:"prevMatch"`
StartSearch Keybinding `yaml:"startSearch"`
MoveWordLeft Keybinding `yaml:"moveWordLeft"` // <alt+left> on Mac
MoveWordRight Keybinding `yaml:"moveWordRight"` // <alt+right> on Mac
BackspaceWord Keybinding `yaml:"backspaceWord"` // <alt+backspace> on Mac
ForwardDeleteWord Keybinding `yaml:"forwardDeleteWord"` // <alt+delete> on Mac
OptionMenu Keybinding `yaml:"optionMenu"`
Select Keybinding `yaml:"select"`
GoInto Keybinding `yaml:"goInto"`
Confirm Keybinding `yaml:"confirm"`
ConfirmMenu Keybinding `yaml:"confirmMenu"`
ConfirmSuggestion Keybinding `yaml:"confirmSuggestion"`
ConfirmInEditor Keybinding `yaml:"confirmInEditor"` // <meta+enter> on Mac
ConfirmInEditorAlt Keybinding `yaml:"confirmInEditor-alt"`
Remove Keybinding `yaml:"remove"`
New Keybinding `yaml:"new"`
Edit Keybinding `yaml:"edit"`
OpenFile Keybinding `yaml:"openFile"`
ScrollUpMain Keybinding `yaml:"scrollUpMain"`
ScrollDownMain Keybinding `yaml:"scrollDownMain"`
ScrollUpMainAlt1 Keybinding `yaml:"scrollUpMain-alt1"`
ScrollDownMainAlt1 Keybinding `yaml:"scrollDownMain-alt1"`
ScrollUpMainAlt2 Keybinding `yaml:"scrollUpMain-alt2"`
ScrollDownMainAlt2 Keybinding `yaml:"scrollDownMain-alt2"`
ExecuteShellCommand Keybinding `yaml:"executeShellCommand"`
CreateRebaseOptionsMenu Keybinding `yaml:"createRebaseOptionsMenu"`
Push Keybinding `yaml:"pushFiles"` // 'Files' appended for legacy reasons
Pull Keybinding `yaml:"pullFiles"` // 'Files' appended for legacy reasons
Refresh Keybinding `yaml:"refresh"`
CreatePatchOptionsMenu Keybinding `yaml:"createPatchOptionsMenu"`
NextTab Keybinding `yaml:"nextTab"`
PrevTab Keybinding `yaml:"prevTab"`
NextScreenMode Keybinding `yaml:"nextScreenMode"`
PrevScreenMode Keybinding `yaml:"prevScreenMode"`
CyclePagers Keybinding `yaml:"cyclePagers"`
Undo Keybinding `yaml:"undo"`
Redo Keybinding `yaml:"redo"`
FilteringMenu Keybinding `yaml:"filteringMenu"`
DiffingMenu Keybinding `yaml:"diffingMenu"`
DiffingMenuAlt Keybinding `yaml:"diffingMenu-alt"`
CopyToClipboard Keybinding `yaml:"copyToClipboard"`
OpenRecentRepos Keybinding `yaml:"openRecentRepos"`
SubmitEditorText Keybinding `yaml:"submitEditorText"`
ExtrasMenu Keybinding `yaml:"extrasMenu"`
ToggleWhitespaceInDiffView Keybinding `yaml:"toggleWhitespaceInDiffView"`
IncreaseContextInDiffView Keybinding `yaml:"increaseContextInDiffView"`
DecreaseContextInDiffView Keybinding `yaml:"decreaseContextInDiffView"`
IncreaseRenameSimilarityThreshold Keybinding `yaml:"increaseRenameSimilarityThreshold"`
DecreaseRenameSimilarityThreshold Keybinding `yaml:"decreaseRenameSimilarityThreshold"`
OpenDiffTool Keybinding `yaml:"openDiffTool"`
}
type KeybindingStatusConfig struct {
@@ -932,7 +932,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
NextBlockAlt: Keybinding{"l"},
PrevBlockAlt2: Keybinding{"<backtab>"},
NextBlockAlt2: Keybinding{"<tab>"},
JumpToBlock: []string{"1", "2", "3", "4", "5"},
JumpToBlock: []Keybinding{{"1"}, {"2"}, {"3"}, {"4"}, {"5"}},
FocusMainView: Keybinding{"0"},
NextMatch: Keybinding{"n"},
PrevMatch: Keybinding{"N"},
+5 -1
View File
@@ -4,6 +4,7 @@ import (
"strings"
"testing"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -127,7 +128,10 @@ func TestUserConfigValidate_enums(t *testing.T) {
{
name: "JumpToBlock keybinding",
setup: func(config *UserConfig, value string) {
config.Keybinding.Universal.JumpToBlock = strings.Split(value, ",")
labels := strings.Split(value, ",")
config.Keybinding.Universal.JumpToBlock = lo.Map(labels, func(label string, _ int) Keybinding {
return Keybinding{label}
})
},
testCases: []testCase{
{value: "", valid: false},
@@ -3,7 +3,6 @@ package controllers
import (
"log"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
@@ -40,7 +39,7 @@ func (self *JumpToSideWindowController) GetKeybindings(opts types.KeybindingsOpt
return &types.Binding{
ViewName: "",
// by default the keys are 1, 2, 3, etc
Keys: opts.GetKeys(config.Keybinding{opts.Config.Universal.JumpToBlock[index]}),
Keys: opts.GetKeys(opts.Config.Universal.JumpToBlock[index]),
Handler: opts.Guards.NoPopupPanel(self.goToSideWindow(window)),
}
})
+6 -5
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/theme"
@@ -210,14 +211,14 @@ func (gui *Gui) configureViewProperties() {
gui.Views.CommitDescription.TextArea.AutoWrapWidth = gui.c.UserConfig().Git.Commit.AutoWrapWidth
if gui.c.UserConfig().Gui.ShowPanelJumps {
keyToTitlePrefix := func(key string) string {
if key == "<disabled>" {
keyToTitlePrefix := func(binding config.Keybinding) string {
if len(binding) == 0 {
return ""
}
return fmt.Sprintf("[%s]", key)
return fmt.Sprintf("[%s]", binding[0])
}
jumpBindings := gui.c.UserConfig().Keybinding.Universal.JumpToBlock
jumpLabels := lo.Map(jumpBindings, func(binding string, _ int) string {
jumpLabels := lo.Map(jumpBindings, func(binding config.Keybinding, _ int) string {
return keyToTitlePrefix(binding)
})
@@ -236,7 +237,7 @@ func (gui *Gui) configureViewProperties() {
gui.Views.Stash.TitlePrefix = jumpLabels[4]
gui.Views.Main.TitlePrefix = keyToTitlePrefix(gui.c.UserConfig().Keybinding.Universal.FocusMainView[0])
gui.Views.Main.TitlePrefix = keyToTitlePrefix(gui.c.UserConfig().Keybinding.Universal.FocusMainView)
} else {
gui.Views.Status.TitlePrefix = ""
+1 -1
View File
@@ -363,7 +363,7 @@ func (self *ViewDriver) Focus() *ViewDriver {
if lo.Contains(window.viewNames, viewName) {
tabIndex := lo.IndexOf(window.viewNames, viewName)
// jump to the desired window
self.t.press(self.t.keys.Universal.JumpToBlock[windowIndex])
self.t.press(self.t.keys.Universal.JumpToBlock[windowIndex][0])
// assert we're in the window before continuing
self.t.assertWithRetries(func() (bool, string) {
@@ -15,9 +15,9 @@ var DisableSwitchTabWithPanelJumpKeys = NewIntegrationTest(NewIntegrationTestArg
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Status().Focus().
Press(config.Keybinding{keys.Universal.JumpToBlock[1]})
Press(keys.Universal.JumpToBlock[1])
t.Views().Files().IsFocused().
Press(config.Keybinding{keys.Universal.JumpToBlock[1]})
Press(keys.Universal.JumpToBlock[1])
// Despite jumping to an already focused panel,
// the tab should not change from the base files view
@@ -16,19 +16,19 @@ var SwitchTabWithPanelJumpKeys = NewIntegrationTest(NewIntegrationTestArgs{
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Worktrees().Focus().
Press(config.Keybinding{keys.Universal.JumpToBlock[2]})
Press(keys.Universal.JumpToBlock[2])
t.Views().Branches().IsFocused().
Press(config.Keybinding{keys.Universal.JumpToBlock[2]})
Press(keys.Universal.JumpToBlock[2])
t.Views().Remotes().IsFocused().
Press(config.Keybinding{keys.Universal.JumpToBlock[2]})
Press(keys.Universal.JumpToBlock[2])
t.Views().Tags().IsFocused().
Press(config.Keybinding{keys.Universal.JumpToBlock[2]})
Press(keys.Universal.JumpToBlock[2])
t.Views().Branches().IsFocused().
Press(config.Keybinding{keys.Universal.JumpToBlock[1]})
Press(keys.Universal.JumpToBlock[1])
// When jumping to a panel from a different one, keep its current tab:
t.Views().Worktrees().IsFocused()
+11 -1
View File
@@ -2473,7 +2473,17 @@
},
"jumpToBlock": {
"items": {
"type": "string"
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
]
},
"type": "array",
"default": [