Skip to content

Commit ff7ea5f

Browse files
jongioCopilot
andcommitted
Add shell completion command
Closes #164 Co-authored-by: Copilot <[email protected]>
1 parent 1b6b733 commit ff7ea5f

4 files changed

Lines changed: 159 additions & 0 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ dispatch
129129
7. Press `s` to cycle sort fields, `S` to flip direction
130130
8. Press `,` to open settings — change theme, launch mode, model, and more
131131

132+
### Shell Completion
133+
134+
Print completion scripts for supported shells:
135+
136+
```sh
137+
dispatch completion bash
138+
dispatch completion zsh
139+
dispatch completion powershell
140+
```
141+
132142
### Key Bindings
133143

134144
#### Navigation

cmd/dispatch/cli.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
5050
}
5151
return true, cleanup, nil
5252

53+
case "completion":
54+
if len(args) < 2 {
55+
err := errors.New("completion requires a shell: bash, zsh, or powershell")
56+
fmt.Fprintf(os.Stderr, "completion: %v\n", err)
57+
return true, cleanup, err
58+
}
59+
if cErr := runCompletion(os.Stdout, args[1]); cErr != nil {
60+
fmt.Fprintf(os.Stderr, "completion: %v\n", cErr)
61+
return true, cleanup, cErr
62+
}
63+
return true, cleanup, nil
64+
5365
case "--demo":
5466
c, demoErr := setupDemo()
5567
if demoErr != nil {
@@ -99,6 +111,81 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
99111
return false, cleanup, nil
100112
}
101113

114+
func runCompletion(w io.Writer, shell string) error {
115+
if w == nil {
116+
w = io.Discard
117+
}
118+
switch strings.ToLower(shell) {
119+
case "bash":
120+
fmt.Fprint(w, bashCompletionScript)
121+
case "zsh":
122+
fmt.Fprint(w, zshCompletionScript)
123+
case "powershell", "pwsh":
124+
fmt.Fprint(w, powershellCompletionScript)
125+
default:
126+
return fmt.Errorf("unsupported shell %q (want bash, zsh, or powershell)", shell)
127+
}
128+
return nil
129+
}
130+
131+
const bashCompletionScript = `# bash completion for dispatch
132+
_dispatch_completion() {
133+
local cur="${COMP_WORDS[COMP_CWORD]}"
134+
local commands="help version update completion"
135+
local flags="-h --help -v --version --demo --clear-cache --reindex"
136+
137+
if [[ "${COMP_CWORD}" -eq 1 ]]; then
138+
COMPREPLY=( $(compgen -W "${commands} ${flags}" -- "${cur}") )
139+
return 0
140+
fi
141+
142+
if [[ "${COMP_WORDS[1]}" == "completion" ]]; then
143+
COMPREPLY=( $(compgen -W "bash zsh powershell" -- "${cur}") )
144+
return 0
145+
fi
146+
}
147+
complete -F _dispatch_completion dispatch disp
148+
`
149+
150+
const zshCompletionScript = `#compdef dispatch disp
151+
_dispatch_completion() {
152+
local -a commands shells flags
153+
commands=(help version update completion)
154+
shells=(bash zsh powershell)
155+
flags=(-h --help -v --version --demo --clear-cache --reindex)
156+
157+
if (( CURRENT == 2 )); then
158+
_describe -t commands 'dispatch command' commands || _describe -t flags 'dispatch flag' flags
159+
return
160+
fi
161+
162+
if [[ ${words[2]} == completion ]]; then
163+
_describe -t shells 'shell' shells
164+
return
165+
fi
166+
}
167+
_dispatch_completion "$@"
168+
`
169+
170+
const powershellCompletionScript = `# PowerShell completion for dispatch
171+
$script:DispatchCommands = @('help', 'version', 'update', 'completion')
172+
$script:DispatchFlags = @('-h', '--help', '-v', '--version', '--demo', '--clear-cache', '--reindex')
173+
$script:DispatchShells = @('bash', 'zsh', 'powershell')
174+
175+
Register-ArgumentCompleter -Native -CommandName dispatch, disp -ScriptBlock {
176+
param($wordToComplete, $commandAst, $cursorPosition)
177+
$tokens = @($commandAst.CommandElements | ForEach-Object { $_.ToString() })
178+
$values = if ($tokens.Count -ge 2 -and $tokens[1] -eq 'completion') {
179+
$script:DispatchShells
180+
} else {
181+
$script:DispatchCommands + $script:DispatchFlags
182+
}
183+
$values |
184+
Where-Object { $_ -like "$wordToComplete*" } |
185+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
186+
}
187+
`
188+
102189
// setupLogRedirect opens the log file (if configured via DISPATCH_LOG) and
103190
// redirects stderr to it. When no log file is configured, stderr is sent to
104191
// os.DevNull to keep Bubble Tea's alt-screen clean. Returns the writer for

cmd/dispatch/cli_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"io"
@@ -83,6 +84,66 @@ func TestHandleArgs_VersionCommand(t *testing.T) {
8384
}
8485
}
8586

87+
func TestRunCompletion_SupportedShells(t *testing.T) {
88+
for _, tc := range []struct {
89+
shell string
90+
want string
91+
}{
92+
{"bash", "complete -F _dispatch_completion dispatch disp"},
93+
{"zsh", "#compdef dispatch disp"},
94+
{"powershell", "Register-ArgumentCompleter"},
95+
{"pwsh", "Register-ArgumentCompleter"},
96+
} {
97+
t.Run(tc.shell, func(t *testing.T) {
98+
var buf bytes.Buffer
99+
if err := runCompletion(&buf, tc.shell); err != nil {
100+
t.Fatalf("runCompletion: %v", err)
101+
}
102+
if !strings.Contains(buf.String(), tc.want) {
103+
t.Errorf("completion output missing %q:\n%s", tc.want, buf.String())
104+
}
105+
})
106+
}
107+
}
108+
109+
func TestRunCompletion_UnsupportedShell(t *testing.T) {
110+
var buf bytes.Buffer
111+
err := runCompletion(&buf, "fish")
112+
if err == nil {
113+
t.Fatal("expected error for unsupported shell")
114+
}
115+
if !strings.Contains(err.Error(), "unsupported shell") {
116+
t.Errorf("error = %v", err)
117+
}
118+
}
119+
120+
func TestHandleArgs_Completion(t *testing.T) {
121+
ch := make(chan *update.UpdateInfo, 1)
122+
123+
done, cleanup, err := handleArgs([]string{"completion", "bash"}, io.Discard, ch)
124+
if err != nil {
125+
t.Fatalf("unexpected error: %v", err)
126+
}
127+
if !done {
128+
t.Error("expected done=true for completion")
129+
}
130+
if cleanup != nil {
131+
t.Error("expected cleanup=nil for completion")
132+
}
133+
}
134+
135+
func TestHandleArgs_CompletionMissingShell(t *testing.T) {
136+
ch := make(chan *update.UpdateInfo, 1)
137+
138+
done, _, err := handleArgs([]string{"completion"}, io.Discard, ch)
139+
if err == nil {
140+
t.Fatal("expected error for missing shell")
141+
}
142+
if !done {
143+
t.Error("expected done=true for completion error")
144+
}
145+
}
146+
86147
func TestHandleArgs_UnknownFlag(t *testing.T) {
87148
ch := make(chan *update.UpdateInfo, 1)
88149

cmd/dispatch/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Usage:
9090
Commands:
9191
help Show this help message
9292
version Print the version
93+
completion <shell> Print shell completion (bash, zsh, powershell)
9394
update Update dispatch to the latest release
9495
9596
Flags:

0 commit comments

Comments
 (0)