-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validate user config keybindings #4275
Changes from all commits
67bb7f6
130801d
f3791e6
a5f78d3
5979b40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: "<f1>", | ||
gocui.KeyF2: "<f2>", | ||
gocui.KeyF3: "<f3>", | ||
gocui.KeyF4: "<f4>", | ||
gocui.KeyF5: "<f5>", | ||
gocui.KeyF6: "<f6>", | ||
gocui.KeyF7: "<f7>", | ||
gocui.KeyF8: "<f8>", | ||
gocui.KeyF9: "<f9>", | ||
gocui.KeyF10: "<f10>", | ||
gocui.KeyF11: "<f11>", | ||
gocui.KeyF12: "<f12>", | ||
gocui.KeyInsert: "<insert>", | ||
gocui.KeyDelete: "<delete>", | ||
gocui.KeyHome: "<home>", | ||
gocui.KeyEnd: "<end>", | ||
gocui.KeyPgup: "<pgup>", | ||
gocui.KeyPgdn: "<pgdown>", | ||
gocui.KeyArrowUp: "<up>", | ||
gocui.KeyShiftArrowUp: "<s-up>", | ||
gocui.KeyArrowDown: "<down>", | ||
gocui.KeyShiftArrowDown: "<s-down>", | ||
gocui.KeyArrowLeft: "<left>", | ||
gocui.KeyArrowRight: "<right>", | ||
gocui.KeyTab: "<tab>", // <c-i> | ||
gocui.KeyBacktab: "<backtab>", | ||
gocui.KeyEnter: "<enter>", // <c-m> | ||
gocui.KeyAltEnter: "<a-enter>", | ||
gocui.KeyEsc: "<esc>", // <c-[>, <c-3> | ||
gocui.KeyBackspace: "<backspace>", // <c-h> | ||
gocui.KeyCtrlSpace: "<c-space>", // <c-~>, <c-2> | ||
gocui.KeyCtrlSlash: "<c-/>", // <c-_> | ||
gocui.KeySpace: "<space>", | ||
gocui.KeyCtrlA: "<c-a>", | ||
gocui.KeyCtrlB: "<c-b>", | ||
gocui.KeyCtrlC: "<c-c>", | ||
gocui.KeyCtrlD: "<c-d>", | ||
gocui.KeyCtrlE: "<c-e>", | ||
gocui.KeyCtrlF: "<c-f>", | ||
gocui.KeyCtrlG: "<c-g>", | ||
gocui.KeyCtrlJ: "<c-j>", | ||
gocui.KeyCtrlK: "<c-k>", | ||
gocui.KeyCtrlL: "<c-l>", | ||
gocui.KeyCtrlN: "<c-n>", | ||
gocui.KeyCtrlO: "<c-o>", | ||
gocui.KeyCtrlP: "<c-p>", | ||
gocui.KeyCtrlQ: "<c-q>", | ||
gocui.KeyCtrlR: "<c-r>", | ||
gocui.KeyCtrlS: "<c-s>", | ||
gocui.KeyCtrlT: "<c-t>", | ||
gocui.KeyCtrlU: "<c-u>", | ||
gocui.KeyCtrlV: "<c-v>", | ||
gocui.KeyCtrlW: "<c-w>", | ||
gocui.KeyCtrlX: "<c-x>", | ||
gocui.KeyCtrlY: "<c-y>", | ||
gocui.KeyCtrlZ: "<c-z>", | ||
gocui.KeyCtrl4: "<c-4>", // <c-\> | ||
gocui.KeyCtrl5: "<c-5>", // <c-]> | ||
gocui.KeyCtrl6: "<c-6>", | ||
gocui.KeyCtrl8: "<c-8>", | ||
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 == "<disabled>" { | ||
return true | ||
} | ||
|
||
if runeCount > 1 { | ||
_, ok := KeyByLabel[strings.ToLower(key)] | ||
return ok | ||
} | ||
|
||
return true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't believe that we have an equivalent to the Particularly because we do That example is starting to seem a bit contrived though.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I briefly thought about adding the command to the error message, but commands can sometimes be very long, so this could be unwieldy. Also, I don't follow your concern about the case sensitive search; the tolower happens inside of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The highlighting of the line of code leads to some confusion on that 2nd part. We do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And if a valid concern, also applies higher up in the file on the other error message. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to the body of this work, but why are
<c-1>
and others not considered valid keys? I see them in the comments up above, but don't know what that means.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comes from commit 2e66d87. The goal of that commit was to ensure that the keys you see in the global
?
menu are exactly what you put in your config file. And the approach it took to ensure this was to have two inverse maps for labelByKey and keyByLabel. The problem with this approach is that there are several labels that map to the same key (e.g. backspace and ctrl-h both map to ascii 8), so you can't put them both in the labelByKey map. We also have #2953 about this.It would be nice to fix this, and I suppose the way to do that is to manually maintain two separate maps for labelToKey and keyToLabel, where labelToKey can have more entries. And then we need to add a field to the
types.Binding
struct that stores the original string so that we can show it in the menu. This would still not allow to bind one action to backspace and another to ctrl-h, but it would at least allow binding things to ctrl-h at all (or ctrl-3). This should really work since those are the actual keys that a user presses to invoke the action. (Although I'm not sure about ctrl-3; that's the same as Esc, and this could be confusing.)