diff --git a/cmd/keys.go b/cmd/keys.go new file mode 100644 index 00000000..aa5a7054 --- /dev/null +++ b/cmd/keys.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/jongio/grut/internal/keybindings" + "github.com/spf13/cobra" +) + +func newKeysCmd() *cobra.Command { + var ( + filter string + asJSON bool + ) + + keysCmd := &cobra.Command{ + Use: "keys", + Short: "Print keybindings", + Long: "Print the built-in keybinding reference without starting the TUI.", + RunE: func(cmd *cobra.Command, args []string) error { + sections := filterKeybindingSections(keybindings.Sections(), filter) + if asJSON { + enc := json.NewEncoder(cmd.OutOrStdout()) + enc.SetIndent("", " ") + return enc.Encode(sections) + } + printKeybindingSections(cmd, sections, filter) + return nil + }, + } + + keysCmd.Flags().StringVar(&filter, "filter", "", "Only show sections, keys, or actions matching this text") + keysCmd.Flags().BoolVar(&asJSON, "json", false, "Print keybindings as JSON") + return keysCmd +} + +func filterKeybindingSections(sections []keybindings.Section, filter string) []keybindings.Section { + query := strings.TrimSpace(strings.ToLower(filter)) + if query == "" { + return sections + } + + var out []keybindings.Section + for _, section := range sections { + if keybindingSectionMatches(section, query) { + out = append(out, section) + continue + } + + filtered := section + filtered.Bindings = nil + for _, binding := range section.Bindings { + if strings.Contains(strings.ToLower(binding.Key), query) || + strings.Contains(strings.ToLower(binding.Action), query) { + filtered.Bindings = append(filtered.Bindings, binding) + } + } + if len(filtered.Bindings) > 0 { + out = append(out, filtered) + } + } + return out +} + +func keybindingSectionMatches(section keybindings.Section, query string) bool { + return strings.Contains(strings.ToLower(section.ID), query) || + strings.Contains(strings.ToLower(section.Title), query) || + strings.Contains(strings.ToLower(section.Description), query) +} + +func printKeybindingSections(cmd *cobra.Command, sections []keybindings.Section, filter string) { + w := cmd.OutOrStdout() + if len(sections) == 0 { + if strings.TrimSpace(filter) == "" { + _, _ = fmt.Fprintln(w, "No keybindings available.") + return + } + _, _ = fmt.Fprintf(w, "No keybindings match %q.\n", filter) + return + } + + for i, section := range sections { + if i > 0 { + _, _ = fmt.Fprintln(w) + } + _, _ = fmt.Fprintf(w, "%s\n", section.Title) + for _, binding := range section.Bindings { + _, _ = fmt.Fprintf(w, " %-14s %s\n", binding.Key, binding.Action) + } + } +} diff --git a/cmd/keys_test.go b/cmd/keys_test.go new file mode 100644 index 00000000..13a70919 --- /dev/null +++ b/cmd/keys_test.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/jongio/grut/internal/keybindings" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKeysCommandPrintsText(t *testing.T) { + cmd := newKeysCmd() + var out bytes.Buffer + cmd.SetOut(&out) + + err := cmd.Execute() + + require.NoError(t, err) + assert.Contains(t, out.String(), "Global") + assert.Contains(t, out.String(), "ctrl+c") +} + +func TestKeysCommandFiltersBindings(t *testing.T) { + cmd := newKeysCmd() + cmd.SetArgs([]string{"--filter", "workflow"}) + var out bytes.Buffer + cmd.SetOut(&out) + + err := cmd.Execute() + + require.NoError(t, err) + assert.Contains(t, out.String(), "Dispatch workflow") + assert.NotContains(t, out.String(), "File Tree\n") +} + +func TestKeysCommandPrintsJSON(t *testing.T) { + cmd := newKeysCmd() + cmd.SetArgs([]string{"--filter", "global", "--json"}) + var out bytes.Buffer + cmd.SetOut(&out) + + err := cmd.Execute() + + require.NoError(t, err) + var sections []keybindings.Section + require.NoError(t, json.Unmarshal(out.Bytes(), §ions)) + require.Len(t, sections, 1) + assert.Equal(t, "Global", sections[0].Title) +} + +func TestRootRegistersKeysCommand(t *testing.T) { + root, cleanup := newRootCommand() + defer cleanup() + + keysCmd, _, err := root.Find([]string{"keys"}) + require.NoError(t, err) + require.NotNil(t, keysCmd) + assert.Equal(t, "keys", keysCmd.Name()) +} diff --git a/cmd/root.go b/cmd/root.go index 9040d589..cfce56d2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -357,6 +357,7 @@ Environment: rootCmd.AddCommand(newRunCmd()) rootCmd.AddCommand(newReportCmd()) rootCmd.AddCommand(newConfigCmd()) + rootCmd.AddCommand(newKeysCmd()) // cleanup releases profiling resources. It is idempotent — safe to call // multiple times (subsequent calls are no-ops).