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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user