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" // Global flags var ( configDirFlag string dataDirFlag string ) // Command classification var commandNames = []string{ "add", "done", "modify", "delete", "start", "stop", "count", "projects", "tags", "info", "edit", "server", "sync", "reports", "setup", } // Report names (dynamically populated) var reportNames = []string{ "active", "all", "completed", "list", "minimal", "newest", "next", "oldest", "overdue", "ready", "recurring", "template", "waiting", } 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: run configured default report (defaults to "list") parsed := getParsedArgs(cmd) // Get default report from config cfg, err := engine.GetConfig() if err != nil { fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) os.Exit(1) } defaultReport := cfg.DefaultReport if defaultReport == "" { defaultReport = "list" } if err := runReport(defaultReport, 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 (check both regular commands and reports) cmdIdx := -1 cmdName := "" for i, arg := range args { // Check regular commands for _, name := range commandNames { if arg == name { cmdIdx = i cmdName = name break } } // Check report names if cmdIdx < 0 { for _, name := range reportNames { 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() { // Add persistent flags for directory overrides rootCmd.PersistentFlags().StringVar(&configDirFlag, "config-dir", "", "Config directory (default: $XDG_CONFIG_HOME/opal or ~/.config/opal)") rootCmd.PersistentFlags().StringVar(&dataDirFlag, "data-dir", "", "Data directory (default: $XDG_DATA_HOME/opal or ~/.local/share/opal)") // Use PersistentPreRun for initialization (runs for all subcommands unless overridden) rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { initializeApp() } // Add regular subcommands rootCmd.AddCommand(addCmd) 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) rootCmd.AddCommand(reportsCmd) // Add report commands dynamically reportCommands := CreateReportCommands() for _, cmd := range reportCommands { rootCmd.AddCommand(cmd) } } func initializeApp() { // Set directory overrides from flags if provided if configDirFlag != "" { engine.SetConfigDirOverride(configDirFlag) } if dataDirFlag != "" { engine.SetDataDirOverride(dataDirFlag) } // Check for first run (before initializing DB/config) isFirstRun := engine.IsFirstRun() // Initialize database if err := engine.InitDB(); err != nil { fmt.Fprintf(os.Stderr, "Error initializing database: %v\n", err) os.Exit(1) } // On first run, create the config file with defaults if isFirstRun { if err := engine.InitConfig(); err != nil { fmt.Fprintf(os.Stderr, "Error creating config: %v\n", err) os.Exit(1) } showFirstRunMessage() } // Load config (reads file if present, otherwise uses defaults) if _, err := engine.LoadConfig(); err != nil { fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) os.Exit(1) } } func showFirstRunMessage() { fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "👋 Welcome to Opal Task Manager!") fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Default configuration created. To customize your setup,") fmt.Fprintln(os.Stderr, "run: opal setup") fmt.Fprintln(os.Stderr, "") }