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