From 801d17865f83076013dde416ac8a9462af332245 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:26:31 -0700 Subject: [PATCH] feat(config): report keybinding conflicts Checks the active keybinding scheme during config validation and prints conflicts with their bound actions. Closes #355 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- cmd/config.go | 35 +++++++++++++++++++++++++++++++++-- cmd/config_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index a0f4b054..e7f8fd4e 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -4,12 +4,14 @@ import ( "fmt" "github.com/jongio/grut/internal/config" + "github.com/jongio/grut/internal/keymap" "github.com/spf13/cobra" ) type ( configLoadFunc func() (*config.Config, error) configPathFunc func() string + keymapLoadFunc func(string) (*keymap.Keymap, error) ) func newConfigCmd() *cobra.Command { @@ -21,22 +23,51 @@ func newConfigCmdWithDeps(load configLoadFunc, path configPathFunc) *cobra.Comma Use: cmdConfig, Short: "Inspect grut configuration", } - configCmd.AddCommand(newConfigCheckCmd(load, path)) + configCmd.AddCommand(newConfigCheckCmdWithKeymap(load, path, keymap.NewKeymap)) return configCmd } func newConfigCheckCmd(load configLoadFunc, path configPathFunc) *cobra.Command { + return newConfigCheckCmdWithKeymap(load, path, keymap.NewKeymap) +} + +func newConfigCheckCmdWithKeymap(load configLoadFunc, path configPathFunc, loadKeymap keymapLoadFunc) *cobra.Command { return &cobra.Command{ Use: "check", Short: "Validate the active grut configuration", RunE: func(cmd *cobra.Command, args []string) error { cfgPath := path() - if _, err := load(); err != nil { + cfg, err := load() + if err != nil { fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", cfgPath) return fmt.Errorf("config check failed: %w", err) } + if err := checkKeybindings(cmd, cfg, cfgPath, loadKeymap); err != nil { + return err + } fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\nOK\n", cfgPath) return nil }, } } + +func checkKeybindings(cmd *cobra.Command, cfg *config.Config, cfgPath string, loadKeymap keymapLoadFunc) error { + scheme := cfg.General.KeybindingScheme + if scheme == "" { + scheme = "default" + } + km, err := loadKeymap(scheme) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", cfgPath) + return fmt.Errorf("config check failed: keybindings: %w", err) + } + conflicts := keymap.DetectConflicts(km.Bindings()) + if len(conflicts) == 0 { + return nil + } + fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\nKeybinding conflicts:\n", cfgPath) + for _, conflict := range conflicts { + fmt.Fprintf(cmd.OutOrStdout(), "- %s\n", conflict.String()) + } + return fmt.Errorf("config check failed: %d keybinding conflict(s) found", len(conflicts)) +} diff --git a/cmd/config_test.go b/cmd/config_test.go index 65617b17..8c223ba4 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/jongio/grut/internal/config" + "github.com/jongio/grut/internal/keymap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -41,6 +42,32 @@ func TestConfigCheckFailure(t *testing.T) { assert.Contains(t, out.String(), `C:\Users\me\AppData\Roaming\grut\config.toml`) } +func TestConfigCheckReportsKeybindingConflicts(t *testing.T) { + cmd := newConfigCheckCmdWithKeymap( + func() (*config.Config, error) { + return &config.Config{General: config.GeneralConfig{KeybindingScheme: "custom"}}, nil + }, + func() string { return `C:\Users\me\AppData\Roaming\grut\config.toml` }, + func(string) (*keymap.Keymap, error) { + return keymap.NewKeymapFromBindings([]keymap.Binding{ + {Key: "x", Mode: keymap.ModePanel, Action: "one"}, + {Key: "x", Mode: keymap.ModePanel, Action: "two"}, + }), nil + }, + ) + var out bytes.Buffer + cmd.SetOut(&out) + + err := cmd.Execute() + + require.Error(t, err) + assert.Contains(t, err.Error(), "keybinding conflict") + assert.Contains(t, out.String(), "Keybinding conflicts") + assert.Contains(t, out.String(), `key "x"`) + assert.Contains(t, out.String(), "one") + assert.Contains(t, out.String(), "two") +} + func TestRootRegistersConfigCheck(t *testing.T) { root, cleanup := newRootCommand() defer cleanup()