fix: shell autocomplete bypasses preprocessing and completes attribute values

Bypass Execute() preprocessing for __complete/__completeNoDesc so Cobra's
built-in completion handles shell TAB without creating tasks. Add root
ValidArgsFunction for flexible syntax (e.g. "opal 1 de<TAB>" → delete),
attribute value completions (status:pending, priority:H, date synonyms),
and NoSpace directive on key: completions to avoid trailing space.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 22:05:15 +01:00
parent 6c28e4d24a
commit 08123aa3c5
2 changed files with 91 additions and 3 deletions
+84 -2
View File
@@ -2,14 +2,37 @@ package cmd
import (
"fmt"
"strings"
"git.jnss.me/joakim/opal/internal/engine"
"github.com/spf13/cobra"
)
// taskFilterCompletion provides dynamic completions for task filter arguments.
// Suggests +tag and project:name completions from the database.
// Suggests +tag, project:name, and attribute value completions from the database.
func taskFilterCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// If typing a key:value, complete the value part
if idx := strings.IndexByte(toComplete, ':'); idx >= 0 {
key := toComplete[:idx]
if engine.ValidAttributeKeys[key] {
return attributeValueCompletions(key, toComplete), cobra.ShellCompDirectiveNoFileComp
}
}
// If toComplete is a prefix of an attribute key, return only key:
// completions with NoSpace so the cursor stays after the colon.
if toComplete != "" && !strings.HasPrefix(toComplete, "+") && !strings.HasPrefix(toComplete, "-") {
var keyCompletions []string
for key := range engine.ValidAttributeKeys {
if strings.HasPrefix(key, toComplete) {
keyCompletions = append(keyCompletions, fmt.Sprintf("%s:", key))
}
}
if len(keyCompletions) > 0 {
return keyCompletions, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
}
}
var completions []string
tags, err := engine.GetAllTags()
@@ -31,10 +54,69 @@ func taskFilterCompletion(cmd *cobra.Command, args []string, toComplete string)
completions = append(completions, fmt.Sprintf("%s:", key))
}
return completions, cobra.ShellCompDirectiveNoFileComp
return completions, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
}
// attributeValueCompletions returns key:value completions for a known attribute key.
// Cobra filters by prefix automatically, so we return all values prefixed with "key:".
func attributeValueCompletions(key, toComplete string) []string {
var values []string
switch key {
case "status":
values = []string{"pending", "completed", "deleted", "recurring"}
case "priority":
values = []string{"H", "M", "L"}
case "project":
projects, err := engine.GetAllProjects()
if err == nil {
values = projects
}
case "due", "wait", "scheduled", "until":
values = []string{
"today", "tomorrow", "yesterday", "now",
"eod", "sow", "eow", "som", "eom",
"mon", "tue", "wed", "thu", "fri", "sat", "sun",
}
case "recur":
values = []string{"daily", "weekly", "monthly", "yearly", "1d", "1w", "2w", "1m", "1y"}
}
completions := make([]string, 0, len(values))
for _, v := range values {
completions = append(completions, fmt.Sprintf("%s:%s", key, v))
}
return completions
}
// rootValidArgsFunction provides completions for root-level arguments,
// enabling flexible syntax like "opal 1 de<TAB>" to complete to "delete".
func rootValidArgsFunction(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Delegate to taskFilterCompletion first — if toComplete is a partial
// attribute key, it returns early with NoSpace and we should honour that.
filterCompletions, directive := taskFilterCompletion(cmd, args, toComplete)
var completions []string
// Suggest command names
for _, name := range commandNames {
completions = append(completions, name)
}
// Suggest report names
for _, name := range reportNames {
completions = append(completions, name)
}
completions = append(completions, filterCompletions...)
return completions, directive
}
func init() {
// Root command completions for flexible syntax (e.g., "opal 1 de<TAB>")
rootCmd.ValidArgsFunction = rootValidArgsFunction
// Register dynamic completions for commands that accept filters
addCmd.ValidArgsFunction = taskFilterCompletion
doneCmd.ValidArgsFunction = taskFilterCompletion
+6
View File
@@ -56,6 +56,7 @@ var rootCmd = &cobra.Command{
Short: "Opal task manager - taskwarrior-inspired CLI task management",
Long: `Opal is a powerful command-line task manager.
It supports filtering, tags, priorities, projects, and recurring tasks.`,
Args: cobra.ArbitraryArgs,
Run: func(cmd *cobra.Command, args []string) {
// Default behavior: run configured default report (defaults to "list")
parsed := getParsedArgs(cmd)
@@ -87,6 +88,11 @@ func Execute() error {
if firstArg == "-h" || firstArg == "--help" || firstArg == "help" {
return rootCmd.Execute()
}
// Let Cobra's built-in completion machinery handle shell completions
// directly, bypassing preprocessing that would create tasks.
if firstArg == "__complete" || firstArg == "__completeNoDesc" {
return rootCmd.Execute()
}
}
// Preprocess arguments (read-only scan — os.Args is never mutated)