jesseduffield.lazygit/pkg/config/user_config_validation.go
Stefan Haller fbcf562e29 Convert custom command Key fields to Keybinding
CustomCommand.Key and CustomCommandMenuOption.Key are user-configured
keybindings just like the built-in ones. Converting them to the Keybinding type
lets a user assign multiple keys to the same custom command, e.g. `key: [a, b]`,
the same way they would for any other keybinding.

The validator iterates over the elements rather than checking a single string,
the binding registration goes through GetValidatedKeyBindingKeys to register
every alternate, and the existing error messages use .String() so a multi-key
binding renders sensibly.

CustomCommandPrompt.Key (a form field name, not a keybinding) stays a plain
string.
2026-05-25 15:32:47 +02:00

191 lines
5.7 KiB
Go

package config
import (
"errors"
"fmt"
"log"
"reflect"
"slices"
"strings"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
func (config *UserConfig) Validate() error {
if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView,
[]string{"dashboard", "allBranchesLog"}); err != nil {
return err
}
if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch,
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
return err
}
if err := validateEnum("gui.fileTreeSortOrder", config.Gui.FileTreeSortOrder,
[]string{"mixed", "filesFirst", "foldersFirst"}); err != nil {
return err
}
if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches,
[]string{"none", "onlyMainBranches", "allBranches"}); err != nil {
return err
}
if err := validateEnum("git.localBranchSortOrder", config.Git.LocalBranchSortOrder,
[]string{"date", "recency", "alphabetical"}); err != nil {
return err
}
if err := validateEnum("git.remoteBranchSortOrder", config.Git.RemoteBranchSortOrder,
[]string{"date", "alphabetical"}); err != nil {
return err
}
if err := validateEnum("git.log.order", config.Git.Log.Order,
[]string{"date-order", "author-date-order", "topo-order", "default"}); err != nil {
return err
}
if err := validateEnum("git.log.showGraph", config.Git.Log.ShowGraph,
[]string{"always", "never", "when-maximised"}); err != nil {
return err
}
if err := validateKeybindings(config.Keybinding); err != nil {
return err
}
if err := validateCustomCommands(config.CustomCommands); err != nil {
return err
}
if err := validateSpinner(config.Gui.Spinner); err != nil {
return err
}
return nil
}
func validateSpinner(spinner SpinnerConfig) error {
if len(spinner.Frames) == 0 {
return errors.New("gui.spinner.frames must not be empty.")
}
firstWidth := utils.StringWidth(spinner.Frames[0])
if lo.SomeBy(spinner.Frames, func(frame string) bool {
return utils.StringWidth(frame) != firstWidth
}) {
return errors.New("All gui.spinner.frames entries must have the same width.")
}
return nil
}
func validateEnum(name string, value string, allowedValues []string) error {
if slices.Contains(allowedValues, value) {
return nil
}
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 := range value.Len() {
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 Keybinding) error {
for _, k := range key {
if !isValidKeybindingKey(k) {
return fmt.Errorf("Unrecognized key '%s' for custom command. For permitted values see %s",
k, constants.Links.Docs.CustomKeybindings)
}
}
return nil
}
func validateCustomCommands(customCommands []CustomCommand) error {
for _, customCommand := range customCommands {
if err := validateCustomCommandKey(customCommand.Key); err != nil {
return err
}
if len(customCommand.CommandMenu) > 0 {
if len(customCommand.Context) > 0 ||
len(customCommand.Command) > 0 ||
len(customCommand.Prompts) > 0 ||
len(customCommand.LoadingText) > 0 ||
len(customCommand.Output) > 0 ||
len(customCommand.OutputTitle) > 0 ||
customCommand.After != nil {
commandRef := ""
if len(customCommand.Key) > 0 {
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key.String())
}
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
}
if err := validateCustomCommands(customCommand.CommandMenu); err != nil {
return err
}
} else {
for _, prompt := range customCommand.Prompts {
if err := validateCustomCommandPrompt(prompt); err != nil {
return err
}
}
if err := validateEnum("customCommand.output", customCommand.Output,
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
return err
}
}
}
return nil
}
func validateCustomCommandPrompt(prompt CustomCommandPrompt) error {
for _, option := range prompt.Options {
for _, k := range option.Key {
if !isValidKeybindingKey(k) {
return fmt.Errorf("Unrecognized key '%s' for custom command prompt option. For permitted values see %s",
k, constants.Links.Docs.CustomKeybindings)
}
}
}
return nil
}