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/completion.go b/completion.go new file mode 100644 index 0000000..acd72a3 --- /dev/null +++ b/completion.go @@ -0,0 +1,80 @@ +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) completionCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "completion", + Short: "Generate the autocompletion script for the specified shell", + 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 the autocompletion script for zsh", + Args: cobra.NoArgs, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return a.completionZsh(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) completionZsh(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/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/generate.go b/generate.go deleted file mode 100644 index 0645ea6..0000000 --- a/generate.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -import ( - _ "embed" -) - -//go:embed _roam -var zshCompletion []byte diff --git a/roam.go b/roam.go index c93c2e9..29cce06 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" ) @@ -80,7 +78,7 @@ func (a *app) command() *cobra.Command { a.setupCommand(), a.updateCommand(), a.syncCommand(), - a.generateCommand(), + a.completionCommand(), ) return root } @@ -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") } 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)