Files
joakim 08123aa3c5 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>
2026-02-25 22:05:15 +01:00

133 lines
4.3 KiB
Go

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, 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()
if err == nil {
for _, tag := range tags {
completions = append(completions, fmt.Sprintf("+%s", tag))
}
}
projects, err := engine.GetAllProjects()
if err == nil {
for _, proj := range projects {
completions = append(completions, fmt.Sprintf("project:%s", proj))
}
}
// Add known attribute keys
for key := range engine.ValidAttributeKeys {
completions = append(completions, fmt.Sprintf("%s:", key))
}
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
deleteCmd.ValidArgsFunction = taskFilterCompletion
modifyCmd.ValidArgsFunction = taskFilterCompletion
startCmd.ValidArgsFunction = taskFilterCompletion
stopCmd.ValidArgsFunction = taskFilterCompletion
editCmd.ValidArgsFunction = taskFilterCompletion
infoCmd.ValidArgsFunction = taskFilterCompletion
annotateCmd.ValidArgsFunction = taskFilterCompletion
denotateCmd.ValidArgsFunction = taskFilterCompletion
logCmd.ValidArgsFunction = taskFilterCompletion
}