diff --git a/cmd/config.go b/cmd/config.go index d8303778..2c9be35f 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/jongio/grut/internal/config" + "github.com/jongio/grut/internal/keymap" toml "github.com/pelletier/go-toml/v2" "github.com/spf13/cobra" ) @@ -15,6 +16,7 @@ import ( type ( configLoadFunc func() (*config.Config, error) configPathFunc func() string + keymapLoadFunc func(string) (*keymap.Keymap, error) ) func newConfigCmd() *cobra.Command { @@ -33,21 +35,52 @@ func newConfigCmdWithDeps(load configLoadFunc, path configPathFunc) *cobra.Comma } 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 }, } } +const defaultKeybindingScheme = "default" + +func checkKeybindings(cmd *cobra.Command, cfg *config.Config, cfgPath string, loadKeymap keymapLoadFunc) error { + scheme := cfg.General.KeybindingScheme + if scheme == "" { + scheme = defaultKeybindingScheme + } + 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)) +} + // newConfigGetCmd builds the "config get" subcommand, which prints a single // resolved configuration value addressed by a dotted key (for example // "git.default_branch" or "preview"). Defaults are applied, so a key the user diff --git a/cmd/config_test.go b/cmd/config_test.go index e55bfab4..9fd6cdaa 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -8,6 +8,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" ) @@ -43,6 +44,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() diff --git a/internal/update/cosign.go b/internal/update/cosign.go index 99a5a128..bed3bf52 100644 --- a/internal/update/cosign.go +++ b/internal/update/cosign.go @@ -32,6 +32,9 @@ const ( // maxCosignArtifactSize limits cosign artifact downloads to 256 KiB. maxCosignArtifactSize int64 = 256 << 10 + + // pemTypeCertificate is the PEM block type for X.509 certificates. + pemTypeCertificate = "CERTIFICATE" ) // Fulcio OIDC extension OIDs per the Sigstore Fulcio certificate specification. @@ -160,7 +163,7 @@ func parseSigstoreBundle(data []byte) ([]byte, []byte, error) { } certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", + Type: pemTypeCertificate, Bytes: derBytes, })