diff --git a/README.md b/README.md index 9ae4d25..34aa6c3 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,16 @@ dispatch 7. Press `s` to cycle sort fields, `S` to flip direction 8. Press `,` to open settings — change theme, launch mode, model, and more +### Shell Completion + +Print completion scripts for supported shells: + +```sh +dispatch completion bash +dispatch completion zsh +dispatch completion powershell +``` + ### Diagnostics Run `dispatch doctor` to print setup checks for the config file, session store, session-state directory, and Copilot CLI binary. diff --git a/cmd/dispatch/cli.go b/cmd/dispatch/cli.go index f877da9..b151085 100644 --- a/cmd/dispatch/cli.go +++ b/cmd/dispatch/cli.go @@ -52,6 +52,18 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd } return true, cleanup, nil + case "completion": + if len(args) < 2 { + err := errors.New("completion requires a shell: bash, zsh, or powershell") + fmt.Fprintf(os.Stderr, "completion: %v\n", err) + return true, cleanup, err + } + if cErr := runCompletion(os.Stdout, args[1]); cErr != nil { + fmt.Fprintf(os.Stderr, "completion: %v\n", cErr) + return true, cleanup, cErr + } + return true, cleanup, nil + case "doctor": runDoctor(os.Stdout) showUpdateNotification(origStderr, updateCh) @@ -106,6 +118,81 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd return false, cleanup, nil } +func runCompletion(w io.Writer, shell string) error { + if w == nil { + w = io.Discard + } + switch strings.ToLower(shell) { + case "bash": + fmt.Fprint(w, bashCompletionScript) + case "zsh": + fmt.Fprint(w, zshCompletionScript) + case "powershell", "pwsh": + fmt.Fprint(w, powershellCompletionScript) + default: + return fmt.Errorf("unsupported shell %q (want bash, zsh, or powershell)", shell) + } + return nil +} + +const bashCompletionScript = `# bash completion for dispatch +_dispatch_completion() { + local cur="${COMP_WORDS[COMP_CWORD]}" + local commands="help version update completion" + local flags="-h --help -v --version --demo --clear-cache --reindex" + + if [[ "${COMP_CWORD}" -eq 1 ]]; then + COMPREPLY=( $(compgen -W "${commands} ${flags}" -- "${cur}") ) + return 0 + fi + + if [[ "${COMP_WORDS[1]}" == "completion" ]]; then + COMPREPLY=( $(compgen -W "bash zsh powershell" -- "${cur}") ) + return 0 + fi +} +complete -F _dispatch_completion dispatch disp +` + +const zshCompletionScript = `#compdef dispatch disp +_dispatch_completion() { + local -a commands shells flags + commands=(help version update completion) + shells=(bash zsh powershell) + flags=(-h --help -v --version --demo --clear-cache --reindex) + + if (( CURRENT == 2 )); then + _describe -t commands 'dispatch command' commands || _describe -t flags 'dispatch flag' flags + return + fi + + if [[ ${words[2]} == completion ]]; then + _describe -t shells 'shell' shells + return + fi +} +_dispatch_completion "$@" +` + +const powershellCompletionScript = `# PowerShell completion for dispatch +$script:DispatchCommands = @('help', 'version', 'update', 'completion') +$script:DispatchFlags = @('-h', '--help', '-v', '--version', '--demo', '--clear-cache', '--reindex') +$script:DispatchShells = @('bash', 'zsh', 'powershell') + +Register-ArgumentCompleter -Native -CommandName dispatch, disp -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + $tokens = @($commandAst.CommandElements | ForEach-Object { $_.ToString() }) + $values = if ($tokens.Count -ge 2 -and $tokens[1] -eq 'completion') { + $script:DispatchShells + } else { + $script:DispatchCommands + $script:DispatchFlags + } + $values | + Where-Object { $_ -like "$wordToComplete*" } | + ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } +} +` + func runDoctor(w io.Writer) { if w == nil { w = io.Discard diff --git a/cmd/dispatch/cli_test.go b/cmd/dispatch/cli_test.go index 7b3ea02..42094d2 100644 --- a/cmd/dispatch/cli_test.go +++ b/cmd/dispatch/cli_test.go @@ -84,6 +84,66 @@ func TestHandleArgs_VersionCommand(t *testing.T) { } } +func TestRunCompletion_SupportedShells(t *testing.T) { + for _, tc := range []struct { + shell string + want string + }{ + {"bash", "complete -F _dispatch_completion dispatch disp"}, + {"zsh", "#compdef dispatch disp"}, + {"powershell", "Register-ArgumentCompleter"}, + {"pwsh", "Register-ArgumentCompleter"}, + } { + t.Run(tc.shell, func(t *testing.T) { + var buf bytes.Buffer + if err := runCompletion(&buf, tc.shell); err != nil { + t.Fatalf("runCompletion: %v", err) + } + if !strings.Contains(buf.String(), tc.want) { + t.Errorf("completion output missing %q:\n%s", tc.want, buf.String()) + } + }) + } +} + +func TestRunCompletion_UnsupportedShell(t *testing.T) { + var buf bytes.Buffer + err := runCompletion(&buf, "fish") + if err == nil { + t.Fatal("expected error for unsupported shell") + } + if !strings.Contains(err.Error(), "unsupported shell") { + t.Errorf("error = %v", err) + } +} + +func TestHandleArgs_Completion(t *testing.T) { + ch := make(chan *update.UpdateInfo, 1) + + done, cleanup, err := handleArgs([]string{"completion", "bash"}, io.Discard, ch) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !done { + t.Error("expected done=true for completion") + } + if cleanup != nil { + t.Error("expected cleanup=nil for completion") + } +} + +func TestHandleArgs_CompletionMissingShell(t *testing.T) { + ch := make(chan *update.UpdateInfo, 1) + + done, _, err := handleArgs([]string{"completion"}, io.Discard, ch) + if err == nil { + t.Fatal("expected error for missing shell") + } + if !done { + t.Error("expected done=true for completion error") + } +} + func TestRunDoctor_PrintsDiagnostics(t *testing.T) { db := filepath.Join(t.TempDir(), "session-store.db") if err := os.WriteFile(db, []byte("sqlite"), 0o600); err != nil { diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go index fc3dbae..754b4ee 100644 --- a/cmd/dispatch/main.go +++ b/cmd/dispatch/main.go @@ -90,6 +90,7 @@ Usage: Commands: help Show this help message version Print the version + completion Print shell completion (bash, zsh, powershell) doctor Print environment diagnostics update Update dispatch to the latest release