From 9f374d8b4fccef0b2e38af49592957f81a3e5fcb Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 23 Sep 2026 12:10:54 +0000 Subject: [PATCH 1/4] Add completion bash to alias roam to git completion Mimic Cobra by printing the bash script to stdout, but just emit complete -F _git roam since roam wraps git. Reviewed-by: Nathan Nutter Reviewed-at: c6168cf67c2720ce643812a0089f13b1a4387f0f --- README.md | 10 +--------- completion.go | 38 ++++++++++++++++++++++++++++++-------- completion_test.go | 8 ++++++++ roam.go | 2 +- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index fb9e7ac..8987e64 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,7 @@ 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 diff --git a/completion.go b/completion.go index cfd9d7c..e25a2a4 100644 --- a/completion.go +++ b/completion.go @@ -14,11 +14,11 @@ import ( //go:embed _roam var zshCompletion []byte -func (a *app) defaultZshOut() string { +func defaultZshOut(a *app) string { return filepath.Join(a.home, ".local/share/zsh/site-functions") } -func (a *app) expandOut(out string) string { +func expandOut(a *app, out string) string { if out == "~" { return a.home } @@ -28,7 +28,7 @@ func (a *app) expandOut(out string) string { 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", @@ -45,10 +45,32 @@ func (a *app) completionCommand() *cobra.Command { }, } var force bool - out := a.defaultZshOut() + out := defaultZshOut(a) bash := &cobra.Command{ - Use: "bash", - Short: "Generate the autocompletion script for bash", + 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, @@ -64,7 +86,7 @@ func (a *app) completionCommand() *cobra.Command { SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, _ []string) error { - return a.completionZsh(force, a.expandOut(out)) + return completionZsh(force, expandOut(a, out)) }, } zsh.Flags().BoolVarP(&force, "force", "f", false, "Overwrite existing generated files") @@ -73,7 +95,7 @@ func (a *app) completionCommand() *cobra.Command { return cmd } -func (a *app) completionZsh(force bool, out string) error { +func completionZsh(force bool, out string) error { if err := os.MkdirAll(out, 0o755); err != nil { return err } diff --git a/completion_test.go b/completion_test.go index f03d5d8..3cb2d47 100644 --- a/completion_test.go +++ b/completion_test.go @@ -59,6 +59,14 @@ 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 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 } From 2ccb8c868cd3fde0c74857256f4d61e9f38db40d Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 23 Sep 2026 13:09:31 +0000 Subject: [PATCH 2/4] Print zsh completion to stdout by default Match Cobra by writing the script to stdout unless --out is given, so users can source or redirect it. Keep the file write with --force when --out is set. --- README.md | 6 +----- completion.go | 35 ++++++++++++++++++++++++++++++++--- completion_test.go | 8 ++++++-- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8987e64..a4d3a88 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,4 @@ 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 e25a2a4..05e9b80 100644 --- a/completion.go +++ b/completion.go @@ -80,13 +80,42 @@ You will need to start a new shell for this setup to take effect.`, }, } zsh := &cobra.Command{ - Use: "zsh", - Short: "Generate the autocompletion script for zsh", + 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 { - return completionZsh(force, expandOut(a, out)) + 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") diff --git a/completion_test.go b/completion_test.go index 3cb2d47..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) { @@ -73,7 +77,7 @@ func TestCompletionZshHelp(t *testing.T) { 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) } } From a6a5db404d8b89e2861800f2aff44b56ec2885bf Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 23 Sep 2026 13:40:30 +0000 Subject: [PATCH 3/4] Refactor: Extract bash subcommand into completion_bash.go Move the bash command construction out of completionCommand so each shell lives in its own file. --- completion.go | 34 +--------------------------------- completion_bash.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 completion_bash.go diff --git a/completion.go b/completion.go index 05e9b80..f0725cd 100644 --- a/completion.go +++ b/completion.go @@ -46,39 +46,7 @@ func completionCommand(a *app) *cobra.Command { } var force bool out := defaultZshOut(a) - bash := &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 - }, - } + bash := completionBashCommand() zsh := &cobra.Command{ Use: "zsh", Short: "Generate the autocompletion script for zsh", 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 + }, + } +} From 730b1c46d49ab709faa67f66bb708f2f0b232de7 Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 23 Sep 2026 13:45:19 +0000 Subject: [PATCH 4/4] Refactor: Extract zsh subcommand into completion_zsh.go Move the zsh command with its file helpers and embedded script out of completion.go, leaving only the parent command. Reviewed-by: Nathan Nutter Reviewed-at: e2d7935505519e1624996ad75be41d577baea07c --- completion.go | 85 +------------------------------------------ completion_zsh.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 84 deletions(-) create mode 100644 completion_zsh.go diff --git a/completion.go b/completion.go index f0725cd..25d3810 100644 --- a/completion.go +++ b/completion.go @@ -1,33 +1,11 @@ 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 completionCommand(a *app) *cobra.Command { cmd := &cobra.Command{ Use: "completion", @@ -44,67 +22,6 @@ func completionCommand(a *app) *cobra.Command { return cmd.Help() }, } - var force bool - out := defaultZshOut(a) - bash := completionBashCommand() - 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)") - cmd.AddCommand(bash, zsh) + cmd.AddCommand(completionBashCommand(), completionZshCommand(a)) return cmd } - -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/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) +}