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", "info", "edit", "server", "sync", } 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 { // Check for help flags/command BEFORE preprocessing // This allows Cobra to handle help naturally for the root command if len(os.Args) > 1 { firstArg := os.Args[1] if firstArg == "-h" || firstArg == "--help" || firstArg == "help" { // Let Cobra handle help - skip preprocessing return rootCmd.Execute() } } // 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) rootCmd.AddCommand(infoCmd) rootCmd.AddCommand(editCmd) } 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) } }