Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions cmd/task/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package main

import (
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/bborn/workflow/internal/db"
)

// newCompletionCmd creates the completion command with subcommands for each shell.
func newCompletionCmd(rootCmd *cobra.Command) *cobra.Command {
completionCmd := &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion scripts",
Long: `Generate shell completion scripts for ty.

To load completions:

Bash:
$ source <(ty completion bash)

# To load completions for each session, execute once:
# Linux:
$ ty completion bash > /etc/bash_completion.d/ty
# macOS:
$ ty completion bash > $(brew --prefix)/etc/bash_completion.d/ty

Zsh:
# 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 for each session, execute once:
$ ty completion zsh > "${fpath[1]}/_ty"

# You will need to start a new shell for this setup to take effect.

Fish:
$ ty completion fish | source

# To load completions for each session, execute once:
$ ty completion fish > ~/.config/fish/completions/ty.fish

PowerShell:
PS> ty completion powershell | Out-String | Invoke-Expression

# To load completions for every new session, run:
PS> ty completion powershell > ty.ps1
# and source this file from your PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
rootCmd.GenZshCompletion(os.Stdout)
case "fish":
rootCmd.GenFishCompletion(os.Stdout, true)
case "powershell":
rootCmd.GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}

return completionCmd
}

// completeTaskIDs returns a completion function that suggests task IDs with their titles.
func completeTaskIDs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return fetchTaskCompletions(toComplete)
}

// completeTaskIDsThenStatus completes task ID for first arg, status for second.
func completeTaskIDsThenStatus(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return fetchTaskCompletions(toComplete)
}
if len(args) == 1 {
return validStatuses(), cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
}

// completeTaskIDsThenProject completes task ID for first arg, project name for second.
func completeTaskIDsThenProject(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return fetchTaskCompletions(toComplete)
}
if len(args) == 1 {
return fetchProjectCompletions()
}
return nil, cobra.ShellCompDirectiveNoFileComp
}

// completeProjectNames returns a completion function for project names.
func completeProjectNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return fetchProjectCompletions()
}

// completeSettingKeys returns a completion function for settings set first arg.
func completeSettingKeys(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{
"anthropic_api_key\tAPI key for ghost text autocomplete",
"autocomplete_enabled\tEnable/disable ghost text (true/false)",
"idle_suspend_timeout\tIdle timeout before suspending (e.g. 6h)",
}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
}

// completeTypeNames returns a completion function for task type names.
func completeTypeNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return fetchTypeCompletions()
}

// completeFlagProjects provides completions for --project flag values.
func completeFlagProjects(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return fetchProjectCompletions()
}

// completeFlagExecutors provides completions for --executor flag values.
func completeFlagExecutors(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
"claude\tAnthropic Claude (default)",
"codex\tOpenAI Codex",
"gemini\tGoogle Gemini",
"pi\tInflection Pi",
"opencode\tOpenCode",
"openclaw\tOpenClaw",
}, cobra.ShellCompDirectiveNoFileComp
}

// completeFlagTypes provides completions for --type flag values.
func completeFlagTypes(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
comps, directive := fetchTypeCompletions()
if len(comps) > 0 {
return comps, directive
}
// Fallback to built-in types if DB unavailable
return []string{"code", "writing", "thinking"}, cobra.ShellCompDirectiveNoFileComp
}

// completeFlagStatuses provides completions for --status flag values.
func completeFlagStatuses(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return validStatuses(), cobra.ShellCompDirectiveNoFileComp
}

// fetchTaskCompletions opens the DB and returns task ID completions.
func fetchTaskCompletions(toComplete string) ([]string, cobra.ShellCompDirective) {
database, err := db.Open(db.DefaultPath())
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
defer database.Close()

tasks, err := database.ListTasks(db.ListTasksOptions{IncludeClosed: true, Limit: 50})
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

var completions []string
for _, t := range tasks {
desc := t.Title
if len(desc) > 40 {
desc = desc[:37] + "..."
}
completions = append(completions, fmt.Sprintf("%d\t[%s] %s", t.ID, t.Status, desc))
}
return completions, cobra.ShellCompDirectiveNoFileComp
}

// fetchProjectCompletions opens the DB and returns project name completions.
func fetchProjectCompletions() ([]string, cobra.ShellCompDirective) {
database, err := db.Open(db.DefaultPath())
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
defer database.Close()

projects, err := database.ListProjects()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

var completions []string
for _, p := range projects {
desc := p.Name
if p.Path != "" {
desc = p.Path
}
completions = append(completions, fmt.Sprintf("%s\t%s", p.Name, desc))
}
return completions, cobra.ShellCompDirectiveNoFileComp
}

// fetchTypeCompletions opens the DB and returns task type completions.
func fetchTypeCompletions() ([]string, cobra.ShellCompDirective) {
database, err := db.Open(db.DefaultPath())
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
defer database.Close()

types, err := database.ListTaskTypes()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

var completions []string
for _, t := range types {
label := t.Label
if label == "" {
label = t.Name
}
completions = append(completions, fmt.Sprintf("%s\t%s", t.Name, label))
}
return completions, cobra.ShellCompDirectiveNoFileComp
}
116 changes: 116 additions & 0 deletions cmd/task/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package main

import (
"os"
"testing"

"github.com/spf13/cobra"
)

func TestNewCompletionCmd(t *testing.T) {
rootCmd := &cobra.Command{Use: "ty"}
completionCmd := newCompletionCmd(rootCmd)

if completionCmd.Use != "completion [bash|zsh|fish|powershell]" {
t.Errorf("unexpected Use: %s", completionCmd.Use)
}

if len(completionCmd.ValidArgs) != 4 {
t.Errorf("expected 4 valid args, got %d", len(completionCmd.ValidArgs))
}

expected := map[string]bool{"bash": true, "zsh": true, "fish": true, "powershell": true}
for _, arg := range completionCmd.ValidArgs {
if !expected[arg] {
t.Errorf("unexpected valid arg: %s", arg)
}
}
}

func TestCompletionCmdOutput(t *testing.T) {
shells := []string{"bash", "zsh", "fish", "powershell"}

for _, shell := range shells {
t.Run(shell, func(t *testing.T) {
rootCmd := &cobra.Command{Use: "ty"}
completionCmd := newCompletionCmd(rootCmd)
rootCmd.AddCommand(completionCmd)

// Capture output
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

rootCmd.SetArgs([]string{"completion", shell})
err := rootCmd.Execute()

w.Close()
os.Stdout = old

if err != nil {
t.Fatalf("completion %s failed: %v", shell, err)
}

buf := make([]byte, 1024)
n, _ := r.Read(buf)
if n == 0 {
t.Errorf("completion %s produced no output", shell)
}
})
}
}

func TestCompleteTaskIDsThenStatus(t *testing.T) {
// When 1 arg already provided (task ID), should return statuses
completions, directive := completeTaskIDsThenStatus(nil, []string{"42"}, "")
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("expected NoFileComp directive")
}

statuses := validStatuses()
if len(completions) != len(statuses) {
t.Errorf("expected %d statuses, got %d", len(statuses), len(completions))
}

// When 2 args already provided, no more completions
completions, _ = completeTaskIDsThenStatus(nil, []string{"42", "done"}, "")
if len(completions) != 0 {
t.Errorf("expected no completions for 2+ args, got %d", len(completions))
}
}

func TestCompleteSettingKeys(t *testing.T) {
completions, directive := completeSettingKeys(nil, []string{}, "")
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("expected NoFileComp directive")
}
if len(completions) != 3 {
t.Errorf("expected 3 setting keys, got %d", len(completions))
}

// After first arg, no more completions
completions, _ = completeSettingKeys(nil, []string{"anthropic_api_key"}, "")
if len(completions) != 0 {
t.Errorf("expected no completions after key, got %d", len(completions))
}
}

func TestCompleteFlagExecutors(t *testing.T) {
completions, directive := completeFlagExecutors(nil, nil, "")
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("expected NoFileComp directive")
}
if len(completions) != 6 {
t.Errorf("expected 6 executors, got %d", len(completions))
}
}

func TestCompleteFlagStatuses(t *testing.T) {
completions, directive := completeFlagStatuses(nil, nil, "")
if directive != cobra.ShellCompDirectiveNoFileComp {
t.Errorf("expected NoFileComp directive")
}
if len(completions) == 0 {
t.Error("expected some status completions")
}
}
Loading
Loading