Fix three critical UX issues in opal-task
Issue 1: Fix recurrence calculation for overdue tasks - Use completion date (End) as base for next instance, not original due date - If task due Monday completed Wednesday, next is Wednesday+7d not Monday+7d - Fallback to Due date if End is not set - Update test to verify new behavior Issue 2: Fix description parsing to work without quotes - Add parseAddArgs() to extract description from non-modifier words - Description = all words that don't start with +, -, or contain : - Enables: opal add buy groceries +shop carrots → 'buy groceries carrots' - Validate description is required (error if only modifiers) - Validate recurring tasks require due date Issue 3: Implement flexible command syntax - Add preprocessArgs() to parse arguments before Cobra routing - Detect command position and split filters (left) from modifiers (right) - Rewrite os.Args so Cobra routes correctly - Enable both 'opal 2 done' and 'opal done 2' syntax - Commands without modifiers accept filters on either side - Commands with modifiers enforce [filters] command [modifiers] - Add confirmation for modify without filters (modifies all tasks) All commands updated to use preprocessed ParsedArgs from context. All tests passing (33 tests).
This commit is contained in:
+121
-2
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -8,14 +9,38 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ParsedArgs represents preprocessed command arguments
|
||||
type ParsedArgs struct {
|
||||
Command string
|
||||
Filters []string
|
||||
Modifiers []string
|
||||
}
|
||||
|
||||
// Context key for parsed args
|
||||
type contextKey string
|
||||
|
||||
const parsedArgsKey contextKey = "parsedArgs"
|
||||
|
||||
// Command classification
|
||||
var commandNames = []string{
|
||||
"add", "list", "done", "modify", "delete",
|
||||
"start", "stop", "count", "projects", "tags",
|
||||
}
|
||||
|
||||
var commandsWithModifiers = map[string]bool{
|
||||
"add": true,
|
||||
"modify": true,
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "opal",
|
||||
Short: "Opal task manager - taskwarrior-inspired CLI task management",
|
||||
Long: `Opal is a powerful command-line task manager inspired by taskwarrior.
|
||||
It supports filtering, tags, priorities, projects, and recurring tasks.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Default behavior: show pending tasks
|
||||
if err := listTasks(args); err != nil {
|
||||
// Default behavior: list tasks
|
||||
parsed := getParsedArgs(cmd)
|
||||
if err := listTasks(parsed.Filters); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -23,9 +48,103 @@ It supports filtering, tags, priorities, projects, and recurring tasks.`,
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
// Preprocess arguments BEFORE Cobra routing
|
||||
if len(os.Args) > 1 {
|
||||
parsed := preprocessArgs(os.Args[1:])
|
||||
|
||||
// Store in context for commands to use
|
||||
ctx := context.WithValue(context.Background(), parsedArgsKey, parsed)
|
||||
rootCmd.SetContext(ctx)
|
||||
|
||||
// Rewrite os.Args for Cobra based on parsed command
|
||||
// This allows Cobra to route to the correct command
|
||||
if parsed.Command != "list" || len(parsed.Filters) > 0 || len(parsed.Modifiers) > 0 {
|
||||
// Reconstruct args: [command, ...filters, ...modifiers]
|
||||
newArgs := []string{os.Args[0], parsed.Command}
|
||||
newArgs = append(newArgs, parsed.Filters...)
|
||||
newArgs = append(newArgs, parsed.Modifiers...)
|
||||
os.Args = newArgs
|
||||
}
|
||||
}
|
||||
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
// getParsedArgs retrieves preprocessed args from context
|
||||
func getParsedArgs(cmd *cobra.Command) *ParsedArgs {
|
||||
if v := cmd.Context().Value(parsedArgsKey); v != nil {
|
||||
if parsed, ok := v.(*ParsedArgs); ok {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return &ParsedArgs{}
|
||||
}
|
||||
|
||||
// preprocessArgs parses command-line arguments before Cobra routing
|
||||
// Returns: command name, filters, modifiers
|
||||
func preprocessArgs(args []string) *ParsedArgs {
|
||||
if len(args) == 0 {
|
||||
return &ParsedArgs{
|
||||
Command: "list", // Default command
|
||||
Filters: []string{},
|
||||
Modifiers: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// Find command position
|
||||
cmdIdx := -1
|
||||
cmdName := ""
|
||||
|
||||
for i, arg := range args {
|
||||
for _, name := range commandNames {
|
||||
if arg == name {
|
||||
cmdIdx = i
|
||||
cmdName = name
|
||||
break
|
||||
}
|
||||
}
|
||||
if cmdIdx >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no command found, treat as filters for default list command
|
||||
if cmdIdx == -1 {
|
||||
return &ParsedArgs{
|
||||
Command: "list",
|
||||
Filters: args,
|
||||
Modifiers: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// Split arguments around command
|
||||
leftArgs := args[:cmdIdx] // Everything before command
|
||||
rightArgs := []string{}
|
||||
if cmdIdx+1 < len(args) {
|
||||
rightArgs = args[cmdIdx+1:] // Everything after command
|
||||
}
|
||||
|
||||
// Determine how to interpret right args
|
||||
if commandsWithModifiers[cmdName] {
|
||||
// Command accepts modifiers
|
||||
// Left = filters, Right = modifiers
|
||||
return &ParsedArgs{
|
||||
Command: cmdName,
|
||||
Filters: leftArgs,
|
||||
Modifiers: rightArgs,
|
||||
}
|
||||
} else {
|
||||
// Command doesn't accept modifiers
|
||||
// Both left and right are filters
|
||||
allFilters := append(leftArgs, rightArgs...)
|
||||
return &ParsedArgs{
|
||||
Command: cmdName,
|
||||
Filters: allFilters,
|
||||
Modifiers: []string{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initializeApp)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user