From 3a6d9789c0937e7bf11b9ee7a47dbdf7153a0156 Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 23 Sep 2026 05:20:17 +0000 Subject: [PATCH 1/2] Refactor: Move generate command into generate.go Isolate the generate subcommand with its helpers so the upcoming rename to the standard Cobra completion command is easy to review. --- generate.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ roam.go | 67 ------------------------------------------------- 2 files changed, 72 insertions(+), 67 deletions(-) diff --git a/generate.go b/generate.go index 0645ea6..585ba8c 100644 --- a/generate.go +++ b/generate.go @@ -2,7 +2,79 @@ package main import ( _ "embed" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" ) //go:embed _roam var zshCompletion []byte + +func (a *app) defaultZshOut() string { + return filepath.Join(a.home, ".local/share/zsh/site-functions") +} + +func (a *app) expandOut(out string) string { + if out == "~" { + return a.home + } + if rest, ok := strings.CutPrefix(out, "~/"); ok { + return filepath.Join(a.home, rest) + } + return out +} + +func (a *app) generateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "generate", + Short: "Generate shell completion files", + SilenceUsage: true, + SilenceErrors: true, + Args: func(_ *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + return fmt.Errorf("unknown generator: %s", args[0]) + }, + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + var force bool + out := a.defaultZshOut() + zsh := &cobra.Command{ + Use: "zsh", + Short: "Generate zsh completion for roam", + Args: cobra.NoArgs, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return a.generateZsh(force, a.expandOut(out)) + }, + } + zsh.Flags().BoolVarP(&force, "force", "f", false, "Overwrite existing generated files") + zsh.Flags().StringVarP(&out, "out", "o", out, "Output directory for generated files (~/.local/share/zsh/site-functions)") + cmd.AddCommand(zsh) + return cmd +} + +func (a *app) generateZsh(force bool, out string) error { + if err := os.MkdirAll(out, 0o755); err != nil { + return err + } + path := filepath.Join(out, "_roam") + _, err := os.Stat(path) + switch { + case err == nil: + if !force { + return fmt.Errorf("%s already exists; use --force to overwrite", path) + } + case !errors.Is(err, os.ErrNotExist): + return err + } + return os.WriteFile(path, zshCompletion, 0o644) +} diff --git a/roam.go b/roam.go index c93c2e9..01e8021 100644 --- a/roam.go +++ b/roam.go @@ -2,14 +2,12 @@ package main import ( "context" - "errors" "fmt" "io" "os" "os/exec" "os/user" "path/filepath" - "strings" "github.com/spf13/cobra" ) @@ -89,71 +87,6 @@ func (a *app) defaultRepo() string { return a.user + "/dotfiles" } -func (a *app) defaultZshOut() string { - return filepath.Join(a.home, ".local/share/zsh/site-functions") -} - -func (a *app) expandOut(out string) string { - if out == "~" { - return a.home - } - if rest, ok := strings.CutPrefix(out, "~/"); ok { - return filepath.Join(a.home, rest) - } - return out -} - -func (a *app) generateCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "generate", - Short: "Generate shell completion files", - SilenceUsage: true, - SilenceErrors: true, - Args: func(_ *cobra.Command, args []string) error { - if len(args) == 0 { - return nil - } - return fmt.Errorf("unknown generator: %s", args[0]) - }, - RunE: func(cmd *cobra.Command, _ []string) error { - return cmd.Help() - }, - } - var force bool - out := a.defaultZshOut() - zsh := &cobra.Command{ - Use: "zsh", - Short: "Generate zsh completion for roam", - Args: cobra.NoArgs, - SilenceUsage: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, _ []string) error { - return a.generateZsh(force, a.expandOut(out)) - }, - } - zsh.Flags().BoolVarP(&force, "force", "f", false, "Overwrite existing generated files") - zsh.Flags().StringVarP(&out, "out", "o", out, "Output directory for generated files (~/.local/share/zsh/site-functions)") - cmd.AddCommand(zsh) - return cmd -} - -func (a *app) generateZsh(force bool, out string) error { - if err := os.MkdirAll(out, 0o755); err != nil { - return err - } - path := filepath.Join(out, "_roam") - _, err := os.Stat(path) - switch { - case err == nil: - if !force { - return fmt.Errorf("%s already exists; use --force to overwrite", path) - } - case !errors.Is(err, os.ErrNotExist): - return err - } - return os.WriteFile(path, zshCompletion, 0o644) -} - func (a *app) gitDir() string { return filepath.Join(a.home, ".dotfiles.git") } From 0b28573b7c9cb5ff709b242b758ebcab44638cc8 Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 23 Sep 2026 05:20:50 +0000 Subject: [PATCH 2/2] Rename generate to completion to mimic Cobra Use the standard Cobra completion subcommand name so users find shell completion where they expect it. Keep the custom zsh script that delegates to git completion for the bare dotfiles repository. Reviewed-by: Nathan Nutter Reviewed-at: 2abcc4ee27325db1066b0c7ca4311c246d7bd1eb --- README.md | 2 +- generate.go => completion.go | 12 +++++----- generate_test.go => completion_test.go | 32 +++++++++++++------------- roam.go | 2 +- roam_test.go | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) rename generate.go => completion.go (82%) rename generate_test.go => completion_test.go (61%) diff --git a/README.md b/README.md index 09e33a5..6efb51d 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Maybe somehow also use, ### Zsh ``` -roam generate zsh +roam completion zsh ``` This writes `_roam` to `~/.local/share/zsh/site-functions`. Use `--out` to choose another directory and `--force` to overwrite an existing file. diff --git a/generate.go b/completion.go similarity index 82% rename from generate.go rename to completion.go index 585ba8c..acd72a3 100644 --- a/generate.go +++ b/completion.go @@ -28,10 +28,10 @@ func (a *app) expandOut(out string) string { return out } -func (a *app) generateCommand() *cobra.Command { +func (a *app) completionCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "generate", - Short: "Generate shell completion files", + Use: "completion", + Short: "Generate the autocompletion script for the specified shell", SilenceUsage: true, SilenceErrors: true, Args: func(_ *cobra.Command, args []string) error { @@ -48,12 +48,12 @@ func (a *app) generateCommand() *cobra.Command { out := a.defaultZshOut() zsh := &cobra.Command{ Use: "zsh", - Short: "Generate zsh completion for roam", + Short: "Generate the autocompletion script for zsh", Args: cobra.NoArgs, SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, _ []string) error { - return a.generateZsh(force, a.expandOut(out)) + return a.completionZsh(force, a.expandOut(out)) }, } zsh.Flags().BoolVarP(&force, "force", "f", false, "Overwrite existing generated files") @@ -62,7 +62,7 @@ func (a *app) generateCommand() *cobra.Command { return cmd } -func (a *app) generateZsh(force bool, out string) error { +func (a *app) completionZsh(force bool, out string) error { if err := os.MkdirAll(out, 0o755); err != nil { return err } diff --git a/generate_test.go b/completion_test.go similarity index 61% rename from generate_test.go rename to completion_test.go index 5870e9a..dcfc4a6 100644 --- a/generate_test.go +++ b/completion_test.go @@ -9,29 +9,29 @@ import ( "github.com/stretchr/testify/require" ) -func TestGenerateZsh(t *testing.T) { +func TestCompletionZsh(t *testing.T) { app, _ := testApp(t) out := filepath.Join(app.home, "completions") - require.NoError(t, app.run(t.Context(), []string{"generate", "zsh", "--out", out})) + require.NoError(t, app.run(t.Context(), []string{"completion", "zsh", "--out", out})) got, err := os.ReadFile(filepath.Join(out, "_roam")) require.NoError(t, err) require.Equal(t, string(zshCompletion), string(got)) } -func TestGenerateZshDefaultOut(t *testing.T) { +func TestCompletionZshDefaultOut(t *testing.T) { app, _ := testApp(t) - require.NoError(t, app.run(t.Context(), []string{"generate", "zsh"})) + require.NoError(t, app.run(t.Context(), []string{"completion", "zsh"})) path := filepath.Join(app.home, ".local/share/zsh/site-functions", "_roam") require.FileExists(t, path) } -func TestGenerateZshExistsWithoutForce(t *testing.T) { +func TestCompletionZshExistsWithoutForce(t *testing.T) { app, _ := testApp(t) out := filepath.Join(app.home, "completions") path := filepath.Join(out, "_roam") require.NoError(t, os.MkdirAll(out, 0o755)) require.NoError(t, os.WriteFile(path, []byte("old\n"), 0o644)) - err := app.run(t.Context(), []string{"generate", "zsh", "-o", out}) + err := app.run(t.Context(), []string{"completion", "zsh", "-o", out}) require.Error(t, err) require.Contains(t, err.Error(), "already exists") got, err := os.ReadFile(path) @@ -39,45 +39,45 @@ func TestGenerateZshExistsWithoutForce(t *testing.T) { require.Equal(t, "old\n", string(got)) } -func TestGenerateZshForce(t *testing.T) { +func TestCompletionZshForce(t *testing.T) { app, _ := testApp(t) out := filepath.Join(app.home, "completions") path := filepath.Join(out, "_roam") require.NoError(t, os.MkdirAll(out, 0o755)) require.NoError(t, os.WriteFile(path, []byte("old\n"), 0o644)) - require.NoError(t, app.run(t.Context(), []string{"generate", "zsh", "--force", "--out=" + out})) + require.NoError(t, app.run(t.Context(), []string{"completion", "zsh", "--force", "--out=" + out})) got, err := os.ReadFile(path) require.NoError(t, err) require.Equal(t, string(zshCompletion), string(got)) } -func TestGenerateZshHelp(t *testing.T) { +func TestCompletionZshHelp(t *testing.T) { app, _ := testApp(t) var stdout bytes.Buffer app.stdout = &stdout - require.NoError(t, app.run(t.Context(), []string{"generate", "zsh", "--help"})) + require.NoError(t, app.run(t.Context(), []string{"completion", "zsh", "--help"})) help := stdout.String() for _, want := range []string{"--force", "--out", "~/.local/share/zsh/site-functions"} { require.Contains(t, help, want) } } -func TestGenerateHelp(t *testing.T) { +func TestCompletionHelp(t *testing.T) { app, _ := testApp(t) var stdout bytes.Buffer app.stdout = &stdout - require.NoError(t, app.run(t.Context(), []string{"generate", "-h"})) + require.NoError(t, app.run(t.Context(), []string{"completion", "-h"})) require.Contains(t, stdout.String(), "zsh") } -func TestGenerateZshTildeOut(t *testing.T) { +func TestCompletionZshTildeOut(t *testing.T) { app, _ := testApp(t) - require.NoError(t, app.run(t.Context(), []string{"generate", "zsh", "-o", "~/custom"})) + require.NoError(t, app.run(t.Context(), []string{"completion", "zsh", "-o", "~/custom"})) path := filepath.Join(app.home, "custom", "_roam") require.FileExists(t, path) } -func TestGenerateUnknown(t *testing.T) { +func TestCompletionUnknown(t *testing.T) { app, _ := testApp(t) - require.Error(t, app.run(t.Context(), []string{"generate", "fish"})) + require.Error(t, app.run(t.Context(), []string{"completion", "fish"})) } diff --git a/roam.go b/roam.go index 01e8021..29cce06 100644 --- a/roam.go +++ b/roam.go @@ -78,7 +78,7 @@ func (a *app) command() *cobra.Command { a.setupCommand(), a.updateCommand(), a.syncCommand(), - a.generateCommand(), + a.completionCommand(), ) return root } diff --git a/roam_test.go b/roam_test.go index 61876e1..1b07243 100644 --- a/roam_test.go +++ b/roam_test.go @@ -91,7 +91,7 @@ func TestRootHelp(t *testing.T) { app.stdout = &stdout require.NoError(t, app.run(t.Context(), tt.args)) help := stdout.String() - for _, want := range []string{"setup", "update", "sync", "generate"} { + for _, want := range []string{"setup", "update", "sync", "completion"} { require.Contains(t, help, want) } _, statErr := os.Stat(logPath)