diff --git a/opal-task/cmd/completions.go b/opal-task/cmd/completions.go index 9d602cf..b310a5e 100644 --- a/opal-task/cmd/completions.go +++ b/opal-task/cmd/completions.go @@ -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" 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") + rootCmd.ValidArgsFunction = rootValidArgsFunction + // Register dynamic completions for commands that accept filters addCmd.ValidArgsFunction = taskFilterCompletion doneCmd.ValidArgsFunction = taskFilterCompletion diff --git a/opal-task/cmd/root.go b/opal-task/cmd/root.go index 40a6951..b4b642e 100644 --- a/opal-task/cmd/root.go +++ b/opal-task/cmd/root.go @@ -54,8 +54,9 @@ var commandsWithModifiers = map[string]bool{ var rootCmd = &cobra.Command{ Use: "opal [filter] [command|report] [modifiers]", 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.`, + 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)