diff --git a/docs/keybindings/Custom_Keybindings.md b/docs/keybindings/Custom_Keybindings.md index 698b87e65..a2537f069 100644 --- a/docs/keybindings/Custom_Keybindings.md +++ b/docs/keybindings/Custom_Keybindings.md @@ -20,11 +20,15 @@ | `` | Pgup | | `` | Pgdn | | `` | ArrowUp | +| `` | ShiftArrowUp | | `` | ArrowDown | +| `` | ShiftArrowDown | | `` | ArrowLeft | | `` | ArrowRight | | `` | Tab | +| `` | Backtab | | `` | Enter | +| `` | AltEnter | | `` | Esc | | `` | Backspace | | `` | CtrlSpace | diff --git a/pkg/config/keynames.go b/pkg/config/keynames.go new file mode 100644 index 000000000..bb9756b43 --- /dev/null +++ b/pkg/config/keynames.go @@ -0,0 +1,93 @@ +package config + +import ( + "strings" + "unicode/utf8" + + "github.com/jesseduffield/gocui" + "github.com/samber/lo" +) + +// NOTE: if you make changes to this table, be sure to update +// docs/keybindings/Custom_Keybindings.md as well + +var LabelByKey = map[gocui.Key]string{ + gocui.KeyF1: "", + gocui.KeyF2: "", + gocui.KeyF3: "", + gocui.KeyF4: "", + gocui.KeyF5: "", + gocui.KeyF6: "", + gocui.KeyF7: "", + gocui.KeyF8: "", + gocui.KeyF9: "", + gocui.KeyF10: "", + gocui.KeyF11: "", + gocui.KeyF12: "", + gocui.KeyInsert: "", + gocui.KeyDelete: "", + gocui.KeyHome: "", + gocui.KeyEnd: "", + gocui.KeyPgup: "", + gocui.KeyPgdn: "", + gocui.KeyArrowUp: "", + gocui.KeyShiftArrowUp: "", + gocui.KeyArrowDown: "", + gocui.KeyShiftArrowDown: "", + gocui.KeyArrowLeft: "", + gocui.KeyArrowRight: "", + gocui.KeyTab: "", // + gocui.KeyBacktab: "", + gocui.KeyEnter: "", // + gocui.KeyAltEnter: "", + gocui.KeyEsc: "", // , + gocui.KeyBackspace: "", // + gocui.KeyCtrlSpace: "", // , + gocui.KeyCtrlSlash: "", // + gocui.KeySpace: "", + gocui.KeyCtrlA: "", + gocui.KeyCtrlB: "", + gocui.KeyCtrlC: "", + gocui.KeyCtrlD: "", + gocui.KeyCtrlE: "", + gocui.KeyCtrlF: "", + gocui.KeyCtrlG: "", + gocui.KeyCtrlJ: "", + gocui.KeyCtrlK: "", + gocui.KeyCtrlL: "", + gocui.KeyCtrlN: "", + gocui.KeyCtrlO: "", + gocui.KeyCtrlP: "", + gocui.KeyCtrlQ: "", + gocui.KeyCtrlR: "", + gocui.KeyCtrlS: "", + gocui.KeyCtrlT: "", + gocui.KeyCtrlU: "", + gocui.KeyCtrlV: "", + gocui.KeyCtrlW: "", + gocui.KeyCtrlX: "", + gocui.KeyCtrlY: "", + gocui.KeyCtrlZ: "", + gocui.KeyCtrl4: "", // + gocui.KeyCtrl5: "", // + gocui.KeyCtrl6: "", + gocui.KeyCtrl8: "", + gocui.MouseWheelUp: "mouse wheel up", + gocui.MouseWheelDown: "mouse wheel down", +} + +var KeyByLabel = lo.Invert(LabelByKey) + +func isValidKeybindingKey(key string) bool { + runeCount := utf8.RuneCountInString(key) + if key == "" { + return true + } + + if runeCount > 1 { + _, ok := KeyByLabel[strings.ToLower(key)] + return ok + } + + return true +} diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 403119ada..735c5aad2 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -2,8 +2,12 @@ package config import ( "fmt" + "log" + "reflect" "slices" "strings" + + "github.com/jesseduffield/lazygit/pkg/constants" ) func (config *UserConfig) Validate() error { @@ -15,6 +19,12 @@ func (config *UserConfig) Validate() error { []string{"none", "onlyArrow", "arrowAndNumber"}); err != nil { return err } + if err := validateKeybindings(config.Keybinding); err != nil { + return err + } + if err := validateCustomCommands(config.CustomCommands); err != nil { + return err + } return nil } @@ -25,3 +35,67 @@ func validateEnum(name string, value string, allowedValues []string) error { allowedValuesStr := strings.Join(allowedValues, ", ") return fmt.Errorf("Unexpected value '%s' for '%s'. Allowed values: %s", value, name, allowedValuesStr) } + +func validateKeybindingsRecurse(path string, node any) error { + value := reflect.ValueOf(node) + if value.Kind() == reflect.Struct { + for _, field := range reflect.VisibleFields(reflect.TypeOf(node)) { + var newPath string + if len(path) == 0 { + newPath = field.Name + } else { + newPath = fmt.Sprintf("%s.%s", path, field.Name) + } + if err := validateKeybindingsRecurse(newPath, + value.FieldByName(field.Name).Interface()); err != nil { + return err + } + } + } else if value.Kind() == reflect.Slice { + for i := 0; i < value.Len(); i++ { + if err := validateKeybindingsRecurse( + fmt.Sprintf("%s[%d]", path, i), value.Index(i).Interface()); err != nil { + return err + } + } + } else if value.Kind() == reflect.String { + key := node.(string) + if !isValidKeybindingKey(key) { + return fmt.Errorf("Unrecognized key '%s' for keybinding '%s'. For permitted values see %s", + key, path, constants.Links.Docs.CustomKeybindings) + } + } else { + log.Fatalf("Unexpected type for property '%s': %s", path, value.Kind()) + } + return nil +} + +func validateKeybindings(keybindingConfig KeybindingConfig) error { + if err := validateKeybindingsRecurse("", keybindingConfig); err != nil { + return err + } + + if len(keybindingConfig.Universal.JumpToBlock) != 5 { + return fmt.Errorf("keybinding.universal.jumpToBlock must have 5 elements; found %d.", + len(keybindingConfig.Universal.JumpToBlock)) + } + + return nil +} + +func validateCustomCommandKey(key string) error { + if !isValidKeybindingKey(key) { + return fmt.Errorf("Unrecognized key '%s' for custom command. For permitted values see %s", + key, constants.Links.Docs.CustomKeybindings) + } + return nil +} + +func validateCustomCommands(customCommands []CustomCommand) error { + for _, customCommand := range customCommands { + if err := validateCustomCommandKey(customCommand.Key); err != nil { + return err + } + } + return nil +} diff --git a/pkg/config/user_config_validation_test.go b/pkg/config/user_config_validation_test.go index 9f7b4d74c..1d8428019 100644 --- a/pkg/config/user_config_validation_test.go +++ b/pkg/config/user_config_validation_test.go @@ -1,6 +1,7 @@ package config import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -29,6 +30,50 @@ func TestUserConfigValidate_enums(t *testing.T) { {value: "invalid_value", valid: false}, }, }, + { + name: "Keybindings", + setup: func(config *UserConfig, value string) { + config.Keybinding.Universal.Quit = value + }, + testCases: []testCase{ + {value: "", valid: true}, + {value: "", valid: true}, + {value: "q", valid: true}, + {value: "", valid: true}, + {value: "invalid_value", valid: false}, + }, + }, + { + name: "JumpToBlock keybinding", + setup: func(config *UserConfig, value string) { + config.Keybinding.Universal.JumpToBlock = strings.Split(value, ",") + }, + testCases: []testCase{ + {value: "", valid: false}, + {value: "1,2,3", valid: false}, + {value: "1,2,3,4,5", valid: true}, + {value: "1,2,3,4,invalid", valid: false}, + {value: "1,2,3,4,5,6", valid: false}, + }, + }, + { + name: "Custom command keybinding", + setup: func(config *UserConfig, value string) { + config.CustomCommands = []CustomCommand{ + { + Key: value, + Command: "echo 'hello'", + }, + } + }, + testCases: []testCase{ + {value: "", valid: true}, + {value: "", valid: true}, + {value: "q", valid: true}, + {value: "", valid: true}, + {value: "invalid_value", valid: false}, + }, + }, } for _, s := range scenarios { diff --git a/pkg/gui/keybindings/keybindings.go b/pkg/gui/keybindings/keybindings.go index da917b82e..923a30c3b 100644 --- a/pkg/gui/keybindings/keybindings.go +++ b/pkg/gui/keybindings/keybindings.go @@ -7,78 +7,11 @@ import ( "unicode/utf8" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/constants" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/samber/lo" ) -var labelByKey = map[gocui.Key]string{ - gocui.KeyF1: "", - gocui.KeyF2: "", - gocui.KeyF3: "", - gocui.KeyF4: "", - gocui.KeyF5: "", - gocui.KeyF6: "", - gocui.KeyF7: "", - gocui.KeyF8: "", - gocui.KeyF9: "", - gocui.KeyF10: "", - gocui.KeyF11: "", - gocui.KeyF12: "", - gocui.KeyInsert: "", - gocui.KeyDelete: "", - gocui.KeyHome: "", - gocui.KeyEnd: "", - gocui.KeyPgup: "", - gocui.KeyPgdn: "", - gocui.KeyArrowUp: "", - gocui.KeyShiftArrowUp: "", - gocui.KeyArrowDown: "", - gocui.KeyShiftArrowDown: "", - gocui.KeyArrowLeft: "", - gocui.KeyArrowRight: "", - gocui.KeyTab: "", // - gocui.KeyBacktab: "", - gocui.KeyEnter: "", // - gocui.KeyAltEnter: "", - gocui.KeyEsc: "", // , - gocui.KeyBackspace: "", // - gocui.KeyCtrlSpace: "", // , - gocui.KeyCtrlSlash: "", // - gocui.KeySpace: "", - gocui.KeyCtrlA: "", - gocui.KeyCtrlB: "", - gocui.KeyCtrlC: "", - gocui.KeyCtrlD: "", - gocui.KeyCtrlE: "", - gocui.KeyCtrlF: "", - gocui.KeyCtrlG: "", - gocui.KeyCtrlJ: "", - gocui.KeyCtrlK: "", - gocui.KeyCtrlL: "", - gocui.KeyCtrlN: "", - gocui.KeyCtrlO: "", - gocui.KeyCtrlP: "", - gocui.KeyCtrlQ: "", - gocui.KeyCtrlR: "", - gocui.KeyCtrlS: "", - gocui.KeyCtrlT: "", - gocui.KeyCtrlU: "", - gocui.KeyCtrlV: "", - gocui.KeyCtrlW: "", - gocui.KeyCtrlX: "", - gocui.KeyCtrlY: "", - gocui.KeyCtrlZ: "", - gocui.KeyCtrl4: "", // - gocui.KeyCtrl5: "", // - gocui.KeyCtrl6: "", - gocui.KeyCtrl8: "", - gocui.MouseWheelUp: "mouse wheel up", - gocui.MouseWheelDown: "mouse wheel down", -} - -var keyByLabel = lo.Invert(labelByKey) - func Label(name string) string { return LabelFromKey(GetKey(name)) } @@ -90,7 +23,7 @@ func LabelFromKey(key types.Key) string { case rune: keyInt = int(key) case gocui.Key: - value, ok := labelByKey[key] + value, ok := config.LabelByKey[key] if ok { return value } @@ -105,7 +38,7 @@ func GetKey(key string) types.Key { if key == "" { return nil } else if runeCount > 1 { - binding, ok := keyByLabel[strings.ToLower(key)] + binding, ok := config.KeyByLabel[strings.ToLower(key)] if !ok { log.Fatalf("Unrecognized key %s for keybinding. For permitted values see %s", strings.ToLower(key), constants.Links.Docs.CustomKeybindings) } else {