a68d701d14
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).
177 lines
4.1 KiB
Go
177 lines
4.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
|
|
"git.jnss.me/joakim/opal/internal/engine"
|
|
"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: list tasks
|
|
parsed := getParsedArgs(cmd)
|
|
if err := listTasks(parsed.Filters); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
},
|
|
}
|
|
|
|
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)
|
|
|
|
// Add subcommands
|
|
rootCmd.AddCommand(addCmd)
|
|
rootCmd.AddCommand(listCmd)
|
|
rootCmd.AddCommand(doneCmd)
|
|
rootCmd.AddCommand(modifyCmd)
|
|
rootCmd.AddCommand(deleteCmd)
|
|
rootCmd.AddCommand(startCmd)
|
|
rootCmd.AddCommand(stopCmd)
|
|
rootCmd.AddCommand(countCmd)
|
|
rootCmd.AddCommand(projectsCmd)
|
|
rootCmd.AddCommand(tagsCmd)
|
|
}
|
|
|
|
func initializeApp() {
|
|
// Initialize database
|
|
if err := engine.InitDB(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error initializing database: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Load config
|
|
if _, err := engine.LoadConfig(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|