diff --git a/README.md b/README.md index fb9e7ac..a4d3a88 100644 --- a/README.md +++ b/README.md @@ -33,20 +33,8 @@ My one slight annoyance is the manual set up required for a new host/account whi ### Bash -```bash -eval "$(roam completion bash)" -``` - -Maybe somehow also use, - -```bash --o bashdefault -o default -o nospace -``` +Run `roam completion bash --help` for instructions. ### 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. +Run `roam completion zsh --help` for instructions. diff --git a/completion.go b/completion.go index cfd9d7c..25d3810 100644 --- a/completion.go +++ b/completion.go @@ -1,34 +1,12 @@ 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 { +func completionCommand(a *app) *cobra.Command { cmd := &cobra.Command{ Use: "completion", Short: "Generate the autocompletion script for the specified shell", @@ -44,48 +22,6 @@ func (a *app) completionCommand() *cobra.Command { return cmd.Help() }, } - var force bool - out := a.defaultZshOut() - bash := &cobra.Command{ - Use: "bash", - Short: "Generate the autocompletion script for bash", - Args: cobra.NoArgs, - SilenceUsage: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, _ []string) error { - _, err := fmt.Fprintln(cmd.OutOrStdout(), "complete -F _git roam") - return err - }, - } - 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(bash, zsh) + cmd.AddCommand(completionBashCommand(), completionZshCommand(a)) 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/completion_bash.go b/completion_bash.go new file mode 100644 index 0000000..c13974a --- /dev/null +++ b/completion_bash.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func completionBashCommand() *cobra.Command { + return &cobra.Command{ + Use: "bash", + Short: "Generate the autocompletion script for bash", + Long: `Generate the autocompletion script for the bash shell. + +This aliases roam completion to git completion, since roam wraps git. +It needs git's bash completion, usually provided by the 'bash-completion' +package. If it is not installed already, you can install it via your OS's +package manager. + +To load completions in your current shell session: + + source <(roam completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + roam completion bash > /etc/bash_completion.d/roam + +#### macOS: + + roam completion bash > $(brew --prefix)/etc/bash_completion.d/roam + +You will need to start a new shell for this setup to take effect.`, + Args: cobra.NoArgs, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, _ []string) error { + _, err := fmt.Fprintln(cmd.OutOrStdout(), "complete -F _git roam") + return err + }, + } +} diff --git a/completion_test.go b/completion_test.go index f03d5d8..03db596 100644 --- a/completion_test.go +++ b/completion_test.go @@ -28,9 +28,13 @@ func TestCompletionZsh(t *testing.T) { func TestCompletionZshDefaultOut(t *testing.T) { app, _ := testApp(t) + var stdout bytes.Buffer + app.stdout = &stdout require.NoError(t, app.run(t.Context(), []string{"completion", "zsh"})) + require.Equal(t, string(zshCompletion), stdout.String()) path := filepath.Join(app.home, ".local/share/zsh/site-functions", "_roam") - require.FileExists(t, path) + _, statErr := os.Stat(path) + require.ErrorIs(t, statErr, os.ErrNotExist) } func TestCompletionZshExistsWithoutForce(t *testing.T) { @@ -59,13 +63,21 @@ func TestCompletionZshForce(t *testing.T) { require.Equal(t, string(zshCompletion), string(got)) } +func TestCompletionBashHelp(t *testing.T) { + app, _ := testApp(t) + var stdout bytes.Buffer + app.stdout = &stdout + require.NoError(t, app.run(t.Context(), []string{"completion", "bash", "--help"})) + require.Contains(t, stdout.String(), "source <(roam completion bash)") +} + func TestCompletionZshHelp(t *testing.T) { app, _ := testApp(t) var stdout bytes.Buffer app.stdout = &stdout 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"} { + for _, want := range []string{"--force", "--out", "~/.local/share/zsh/site-functions", "source <(roam completion zsh)"} { require.Contains(t, help, want) } } diff --git a/completion_zsh.go b/completion_zsh.go new file mode 100644 index 0000000..d23ea7d --- /dev/null +++ b/completion_zsh.go @@ -0,0 +1,93 @@ +package main + +import ( + _ "embed" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +//go:embed _roam +var zshCompletion []byte + +func defaultZshOut(a *app) string { + return filepath.Join(a.home, ".local/share/zsh/site-functions") +} + +func expandOut(a *app, out string) string { + if out == "~" { + return a.home + } + if rest, ok := strings.CutPrefix(out, "~/"); ok { + return filepath.Join(a.home, rest) + } + return out +} + +func completionZshCommand(a *app) *cobra.Command { + var force bool + out := defaultZshOut(a) + zsh := &cobra.Command{ + Use: "zsh", + Short: "Generate the autocompletion script for zsh", + Long: `Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(roam completion zsh) + +To load completions for every new session, execute once: + +#### Linux: + + roam completion zsh > "${fpath[1]}/_roam" + +#### macOS: + + roam completion zsh > $(brew --prefix)/share/zsh/site-functions/_roam + +You will need to start a new shell for this setup to take effect. + +Alternatively, use --out to write _roam to a directory directly, +with --force to overwrite an existing file.`, + Args: cobra.NoArgs, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, _ []string) error { + if cmd.Flags().Changed("out") { + return completionZsh(force, expandOut(a, out)) + } + _, err := cmd.OutOrStdout().Write(zshCompletion) + return err + }, + } + 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)") + return zsh +} + +func 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/roam.go b/roam.go index 29cce06..4c662ee 100644 --- a/roam.go +++ b/roam.go @@ -78,7 +78,7 @@ func (a *app) command() *cobra.Command { a.setupCommand(), a.updateCommand(), a.syncCommand(), - a.completionCommand(), + completionCommand(a), ) return root }