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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.jnss.me/joakim/opal/internal/engine"
|
"git.jnss.me/joakim/opal/internal/engine"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// taskFilterCompletion provides dynamic completions for task filter arguments.
|
// 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) {
|
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
|
var completions []string
|
||||||
|
|
||||||
tags, err := engine.GetAllTags()
|
tags, err := engine.GetAllTags()
|
||||||
@@ -31,10 +54,69 @@ func taskFilterCompletion(cmd *cobra.Command, args []string, toComplete string)
|
|||||||
completions = append(completions, fmt.Sprintf("%s:", key))
|
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() {
|
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
|
// Register dynamic completions for commands that accept filters
|
||||||
addCmd.ValidArgsFunction = taskFilterCompletion
|
addCmd.ValidArgsFunction = taskFilterCompletion
|
||||||
doneCmd.ValidArgsFunction = taskFilterCompletion
|
doneCmd.ValidArgsFunction = taskFilterCompletion
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "Opal task manager - taskwarrior-inspired CLI task management",
|
Short: "Opal task manager - taskwarrior-inspired CLI task management",
|
||||||
Long: `Opal is a powerful command-line task manager.
|
Long: `Opal is a powerful command-line task manager.
|
||||||
It supports filtering, tags, priorities, projects, and recurring tasks.`,
|
It supports filtering, tags, priorities, projects, and recurring tasks.`,
|
||||||
|
Args: cobra.ArbitraryArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Default behavior: run configured default report (defaults to "list")
|
// Default behavior: run configured default report (defaults to "list")
|
||||||
parsed := getParsedArgs(cmd)
|
parsed := getParsedArgs(cmd)
|
||||||
@@ -87,6 +88,11 @@ func Execute() error {
|
|||||||
if firstArg == "-h" || firstArg == "--help" || firstArg == "help" {
|
if firstArg == "-h" || firstArg == "--help" || firstArg == "help" {
|
||||||
return rootCmd.Execute()
|
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)
|
// Preprocess arguments (read-only scan — os.Args is never mutated)
|
||||||
|
|||||||
Reference in New Issue
Block a user