Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 97 additions & 21 deletions cmd/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"encoding/json"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -41,23 +42,80 @@ func newConfigCheckCmd(load configLoadFunc, path configPathFunc) *cobra.Command
}

func newConfigCheckCmdWithKeymap(load configLoadFunc, path configPathFunc, loadKeymap keymapLoadFunc) *cobra.Command {
return &cobra.Command{
Use: "check",
Short: "Validate the active grut configuration",
var asJSON bool
cmd := &cobra.Command{
Use: "check",
Short: "Validate the active grut configuration",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
cfgPath := path()
cfg, err := load()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", cfgPath)
return fmt.Errorf("config check failed: %w", err)
return runConfigCheck(cmd, load, path, loadKeymap, asJSON)
},
}
cmd.Flags().BoolVar(&asJSON, "json", false, "Output the config check report as JSON")
return cmd
}

type configCheckReport struct {
OK bool `json:"ok"`
ConfigPath string `json:"config_path"`
Error string `json:"error,omitempty"`
KeybindingConflicts []keybindingConflict `json:"keybinding_conflicts"`
}

type keybindingConflict struct {
Key string `json:"key"`
Mode string `json:"mode"`
Context string `json:"context"`
Actions []string `json:"actions"`
}

func runConfigCheck(cmd *cobra.Command, load configLoadFunc, path configPathFunc, loadKeymap keymapLoadFunc, asJSON bool) error {
cfgPath := path()
report := configCheckReport{ConfigPath: cfgPath, KeybindingConflicts: []keybindingConflict{}}
cfg, err := load()
if err != nil {
report.Error = err.Error()
if asJSON {
if encodeErr := writeConfigCheckJSON(cmd.OutOrStdout(), report); encodeErr != nil {
return encodeErr
}
if err := checkKeybindings(cmd, cfg, cfgPath, loadKeymap); err != nil {
return err
} else {
fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", cfgPath)
}
return fmt.Errorf("config check failed: %w", err)
}

conflicts, err := detectConfigKeybindingConflicts(cfg, loadKeymap)
if err != nil {
report.Error = fmt.Sprintf("keybindings: %v", err)
if asJSON {
if encodeErr := writeConfigCheckJSON(cmd.OutOrStdout(), report); encodeErr != nil {
return encodeErr
}
fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\nOK\n", cfgPath)
return nil
},
} else {
fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", cfgPath)
}
return fmt.Errorf("config check failed: keybindings: %w", err)
}
report.KeybindingConflicts = conflicts
if len(conflicts) > 0 {
report.Error = fmt.Sprintf("%d keybinding conflict(s) found", len(conflicts))
if asJSON {
if encodeErr := writeConfigCheckJSON(cmd.OutOrStdout(), report); encodeErr != nil {
return encodeErr
}
} else {
writeKeybindingConflicts(cmd.OutOrStdout(), cfgPath, conflicts)
}
return fmt.Errorf("config check failed: %d keybinding conflict(s) found", len(conflicts))
}

report.OK = true
if asJSON {
return writeConfigCheckJSON(cmd.OutOrStdout(), report)
}
fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\nOK\n", cfgPath)
return nil
}

const configPathCommandName = "path"
Expand All @@ -75,25 +133,43 @@ func newConfigPathCmd(path configPathFunc, dataPath dataPathFunc) *cobra.Command

const defaultKeybindingScheme = "default"

func checkKeybindings(cmd *cobra.Command, cfg *config.Config, cfgPath string, loadKeymap keymapLoadFunc) error {
func detectConfigKeybindingConflicts(cfg *config.Config, loadKeymap keymapLoadFunc) ([]keybindingConflict, 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)
return nil, err
}
conflicts := keymap.DetectConflicts(km.Bindings())
if len(conflicts) == 0 {
return nil
out := make([]keybindingConflict, 0, len(conflicts))
for _, conflict := range conflicts {
out = append(out, keybindingConflict{
Key: conflict.Key,
Mode: conflict.Mode.String(),
Context: conflict.Context,
Actions: conflict.Actions,
})
}
fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\nKeybinding conflicts:\n", cfgPath)
return out, nil
}

func writeKeybindingConflicts(out io.Writer, cfgPath string, conflicts []keybindingConflict) {
fmt.Fprintf(out, "Config: %s\nKeybinding conflicts:\n", cfgPath)
for _, conflict := range conflicts {
fmt.Fprintf(cmd.OutOrStdout(), "- %s\n", conflict.String())
ctx := conflict.Context
if ctx == "" {
ctx = "(all)"
}
fmt.Fprintf(out, "- key %q in mode %s context %s: actions %v\n", conflict.Key, conflict.Mode, ctx, conflict.Actions)
}
return fmt.Errorf("config check failed: %d keybinding conflict(s) found", len(conflicts))
}

func writeConfigCheckJSON(out io.Writer, report configCheckReport) error {
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
return enc.Encode(report)
}

// newConfigGetCmd builds the "config get" subcommand, which prints a single
Expand Down
86 changes: 86 additions & 0 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
Expand Down Expand Up @@ -33,6 +34,71 @@ func TestConfigCheckSuccess(t *testing.T) {
assert.Contains(t, out.String(), "OK")
}

func TestConfigCheckJSONSuccess(t *testing.T) {
cmd := newConfigCheckCmd(
func() (*config.Config, error) { return &config.Config{}, nil },
func() string { return windowsConfigPath },
)
cmd.SetArgs([]string{"--json"})
var out bytes.Buffer
cmd.SetOut(&out)

err := cmd.Execute()

require.NoError(t, err)
var report configCheckReport
require.NoError(t, json.Unmarshal(out.Bytes(), &report))
assert.True(t, report.OK)
assert.Equal(t, windowsConfigPath, report.ConfigPath)
assert.Empty(t, report.KeybindingConflicts)
}

func TestConfigCheckJSONFailure(t *testing.T) {
cmd := newConfigCheckCmd(
func() (*config.Config, error) { return nil, errors.New("config preview.width: must be 1-100") },
func() string { return windowsConfigPath },
)
cmd.SetArgs([]string{"--json"})
var out bytes.Buffer
cmd.SetOut(&out)

err := cmd.Execute()

require.Error(t, err)
var report configCheckReport
require.NoError(t, json.Unmarshal(out.Bytes(), &report))
assert.False(t, report.OK)
assert.Equal(t, windowsConfigPath, report.ConfigPath)
assert.Contains(t, report.Error, "preview.width")
}

func TestConfigCheckJSONReportsKeybindingConflicts(t *testing.T) {
cmd := newConfigCheckCmdWithKeymap(
func() (*config.Config, error) {
return &config.Config{General: config.GeneralConfig{KeybindingScheme: "custom"}}, nil
},
func() string { return windowsConfigPath },
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
},
)
cmd.SetArgs([]string{"--json"})
var out bytes.Buffer
cmd.SetOut(&out)

err := cmd.Execute()

require.Error(t, err)
var report configCheckReport
require.NoError(t, json.Unmarshal(out.Bytes(), &report))
require.Len(t, report.KeybindingConflicts, 1)
assert.Equal(t, "x", report.KeybindingConflicts[0].Key)
assert.Equal(t, []string{"one", "two"}, report.KeybindingConflicts[0].Actions)
}

func TestConfigCheckFailure(t *testing.T) {
cmd := newConfigCheckCmd(
func() (*config.Config, error) { return nil, errors.New("config preview.width: must be 1-100") },
Expand Down Expand Up @@ -75,6 +141,26 @@ func TestConfigCheckReportsKeybindingConflicts(t *testing.T) {
assert.Contains(t, out.String(), "two")
}

func TestConfigCheckKeymapLoadFailurePrintsConfigPath(t *testing.T) {
cmd := newConfigCheckCmdWithKeymap(
func() (*config.Config, error) {
return &config.Config{General: config.GeneralConfig{KeybindingScheme: "custom"}}, nil
},
func() string { return windowsConfigPath },
func(string) (*keymap.Keymap, error) {
return nil, errors.New("scheme not found")
},
)
var out bytes.Buffer
cmd.SetOut(&out)

err := cmd.Execute()

require.Error(t, err)
assert.Contains(t, err.Error(), "keybindings")
assert.Contains(t, out.String(), windowsConfigPath)
}

func TestConfigPathPrintsResolvedPaths(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading