Add info and edit commands for interactive task management
- Add 'opal info' command to display detailed task information - Shows all task attributes including UUID, timestamps, and metadata - Supports flexible syntax (opal info 2 or opal 2 info) - Displays recurrence information and parent UUID for recurring tasks - Add 'opal edit' command to edit tasks in $EDITOR - Opens task in text editor with human-readable format - Supports all editable fields with smart date formatting - Special handling for recurring tasks (updates parent template) - Status changes trigger appropriate methods (Complete/Delete) - Auto-saves changes on editor exit without confirmation - Clear validation and error messages - Register new commands in root command dispatcher
This commit is contained in:
@@ -0,0 +1,536 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.jnss.me/joakim/opal/internal/engine"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var editCmd = &cobra.Command{
|
||||
Use: "edit [filter...]",
|
||||
Short: "Edit a task in $EDITOR",
|
||||
Long: `Opens a task in your $EDITOR for interactive modification.
|
||||
|
||||
Examples:
|
||||
opal edit 2 # Edit task with display ID 2
|
||||
opal 2 edit # Flexible syntax (same as above)
|
||||
opal edit +urgent # Edit task if only one matches`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
parsed := getParsedArgs(cmd)
|
||||
if err := editTask(parsed.Filters); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func editTask(args []string) error {
|
||||
// Validate we have a filter
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("no task specified for edit command")
|
||||
}
|
||||
|
||||
// Parse filter
|
||||
filter, err := engine.ParseFilter(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse filter: %w", err)
|
||||
}
|
||||
|
||||
// Load working set to resolve IDs
|
||||
ws, err := engine.LoadWorkingSet()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load working set: %w", err)
|
||||
}
|
||||
|
||||
// Resolve task
|
||||
var task *engine.Task
|
||||
|
||||
if len(filter.IDs) > 0 {
|
||||
// Resolve display ID (should be exactly one)
|
||||
if len(filter.IDs) != 1 {
|
||||
return fmt.Errorf("edit requires exactly one task (specified %d IDs)", len(filter.IDs))
|
||||
}
|
||||
task, err = ws.GetTaskByDisplayID(filter.IDs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Use filter to get tasks
|
||||
tasks, err := engine.GetTasks(filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get tasks: %w", err)
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return fmt.Errorf("no task found matching filter")
|
||||
}
|
||||
if len(tasks) > 1 {
|
||||
return fmt.Errorf("edit requires exactly one task (filter matched %d tasks)", len(tasks))
|
||||
}
|
||||
|
||||
task = tasks[0]
|
||||
}
|
||||
|
||||
// Create temporary file
|
||||
tmpFile, err := os.CreateTemp("", "opal-task-*.txt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath) // Clean up
|
||||
|
||||
// Write task data to temp file
|
||||
content := generateEditableContent(task)
|
||||
if _, err := tmpFile.WriteString(content); err != nil {
|
||||
tmpFile.Close()
|
||||
return fmt.Errorf("failed to write temp file: %w", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
// Launch editor
|
||||
if err := launchEditor(tmpPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse edited content
|
||||
fields, err := parseEditedFile(tmpPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply changes to task
|
||||
if err := applyEditedFields(task, fields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Task %s updated.\n", task.UUID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateEditableContent creates the editable text format
|
||||
func generateEditableContent(task *engine.Task) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Header with read-only info
|
||||
sb.WriteString(fmt.Sprintf("# Task UUID: %s (read-only)\n", task.UUID))
|
||||
sb.WriteString(fmt.Sprintf("# Created: %s (read-only)\n", formatTimeForEdit(task.Created)))
|
||||
sb.WriteString(fmt.Sprintf("# Modified: %s (read-only)\n", formatTimeForEdit(task.Modified)))
|
||||
|
||||
if task.ParentUUID != nil {
|
||||
sb.WriteString(fmt.Sprintf("# Parent UUID: %s (this is a recurring task instance)\n", *task.ParentUUID))
|
||||
sb.WriteString("# Note: Changing 'recurrence' will update the template for future instances\n")
|
||||
}
|
||||
|
||||
if task.End != nil {
|
||||
sb.WriteString(fmt.Sprintf("# End: %s (read-only, set on completion)\n", formatTimeForEdit(*task.End)))
|
||||
}
|
||||
|
||||
sb.WriteString("#\n")
|
||||
sb.WriteString("# Edit the fields below. Lines starting with # are ignored.\n")
|
||||
sb.WriteString("# Leave a value empty to clear it.\n")
|
||||
sb.WriteString("# Status: pending, completed, deleted (recurring/template is system-managed)\n")
|
||||
sb.WriteString("# Priority: H (high), M (medium), L (low), D (default)\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Editable fields
|
||||
sb.WriteString(fmt.Sprintf("description: %s\n", task.Description))
|
||||
sb.WriteString(fmt.Sprintf("status: %s\n", formatStatusForEdit(task.Status)))
|
||||
sb.WriteString(fmt.Sprintf("priority: %s\n", formatPriorityForEdit(task.Priority)))
|
||||
|
||||
if task.Project != nil {
|
||||
sb.WriteString(fmt.Sprintf("project: %s\n", *task.Project))
|
||||
} else {
|
||||
sb.WriteString("project: \n")
|
||||
}
|
||||
|
||||
if task.Due != nil {
|
||||
sb.WriteString(fmt.Sprintf("due: %s\n", formatTimeForEdit(*task.Due)))
|
||||
} else {
|
||||
sb.WriteString("due: \n")
|
||||
}
|
||||
|
||||
if task.Scheduled != nil {
|
||||
sb.WriteString(fmt.Sprintf("scheduled: %s\n", formatTimeForEdit(*task.Scheduled)))
|
||||
} else {
|
||||
sb.WriteString("scheduled: \n")
|
||||
}
|
||||
|
||||
if task.Wait != nil {
|
||||
sb.WriteString(fmt.Sprintf("wait: %s\n", formatTimeForEdit(*task.Wait)))
|
||||
} else {
|
||||
sb.WriteString("wait: \n")
|
||||
}
|
||||
|
||||
if task.Until != nil {
|
||||
sb.WriteString(fmt.Sprintf("until: %s\n", formatTimeForEdit(*task.Until)))
|
||||
} else {
|
||||
sb.WriteString("until: \n")
|
||||
}
|
||||
|
||||
if task.Start != nil {
|
||||
sb.WriteString(fmt.Sprintf("start: %s\n", formatTimeForEdit(*task.Start)))
|
||||
} else {
|
||||
sb.WriteString("start: \n")
|
||||
}
|
||||
|
||||
// Recurrence - show template's recurrence or instance's parent recurrence
|
||||
var recurrenceValue string
|
||||
if task.RecurrenceDuration != nil {
|
||||
recurrenceValue = engine.FormatRecurrenceDuration(*task.RecurrenceDuration)
|
||||
} else if task.ParentUUID != nil {
|
||||
// Load parent to show its recurrence
|
||||
if parent, err := engine.GetTask(*task.ParentUUID); err == nil && parent.RecurrenceDuration != nil {
|
||||
recurrenceValue = engine.FormatRecurrenceDuration(*parent.RecurrenceDuration)
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("recurrence: %s\n", recurrenceValue))
|
||||
|
||||
// Tags
|
||||
if len(task.Tags) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("tags: %s\n", strings.Join(task.Tags, ",")))
|
||||
} else {
|
||||
sb.WriteString("tags: \n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// parseEditedFile reads and parses the modified file
|
||||
func parseEditedFile(filepath string) (map[string]string, error) {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open edited file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fields := make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
lineNum := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Text()
|
||||
|
||||
// Skip comments and empty lines
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse "key: value"
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("line %d: invalid format (expected 'field: value')", lineNum)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
fields[key] = value
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error reading file: %w", err)
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// applyEditedFields applies parsed changes to task
|
||||
func applyEditedFields(task *engine.Task, fields map[string]string) error {
|
||||
// Validate required fields
|
||||
description, hasDesc := fields["description"]
|
||||
if !hasDesc || strings.TrimSpace(description) == "" {
|
||||
return fmt.Errorf("description cannot be empty")
|
||||
}
|
||||
|
||||
// Parse status
|
||||
var newStatus engine.Status
|
||||
if statusStr, ok := fields["status"]; ok && statusStr != "" {
|
||||
parsed, err := parseStatus(statusStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newStatus = parsed
|
||||
} else {
|
||||
newStatus = task.Status
|
||||
}
|
||||
|
||||
// Handle status changes specially
|
||||
oldStatus := task.Status
|
||||
|
||||
// If changing to completed, use Complete() method
|
||||
if newStatus == engine.StatusCompleted && oldStatus != engine.StatusCompleted {
|
||||
// Apply other fields first
|
||||
if err := applyNonStatusFields(task, fields); err != nil {
|
||||
return err
|
||||
}
|
||||
// Then complete (which saves automatically)
|
||||
return task.Complete()
|
||||
}
|
||||
|
||||
// If changing to deleted, use Delete() method
|
||||
if newStatus == engine.StatusDeleted && oldStatus != engine.StatusDeleted {
|
||||
// Apply other fields first
|
||||
if err := applyNonStatusFields(task, fields); err != nil {
|
||||
return err
|
||||
}
|
||||
// Then delete (which saves automatically)
|
||||
return task.Delete(false)
|
||||
}
|
||||
|
||||
// If changing from completed/deleted to pending, clear end time
|
||||
if newStatus == engine.StatusPending && (oldStatus == engine.StatusCompleted || oldStatus == engine.StatusDeleted) {
|
||||
task.Status = engine.StatusPending
|
||||
task.End = nil
|
||||
} else {
|
||||
task.Status = newStatus
|
||||
}
|
||||
|
||||
// Apply all other fields
|
||||
if err := applyNonStatusFields(task, fields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the task
|
||||
return task.Save()
|
||||
}
|
||||
|
||||
// applyNonStatusFields applies all fields except status
|
||||
func applyNonStatusFields(task *engine.Task, fields map[string]string) error {
|
||||
// Description
|
||||
if desc, ok := fields["description"]; ok {
|
||||
task.Description = desc
|
||||
}
|
||||
|
||||
// Priority
|
||||
if priStr, ok := fields["priority"]; ok && priStr != "" {
|
||||
pri, err := parsePriority(priStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.Priority = pri
|
||||
}
|
||||
|
||||
// Project
|
||||
if proj, ok := fields["project"]; ok {
|
||||
if proj == "" {
|
||||
task.Project = nil
|
||||
} else {
|
||||
task.Project = &proj
|
||||
}
|
||||
}
|
||||
|
||||
// Date fields
|
||||
dateFields := map[string]**time.Time{
|
||||
"due": &task.Due,
|
||||
"scheduled": &task.Scheduled,
|
||||
"wait": &task.Wait,
|
||||
"until": &task.Until,
|
||||
"start": &task.Start,
|
||||
}
|
||||
|
||||
for fieldName, taskField := range dateFields {
|
||||
if dateStr, ok := fields[fieldName]; ok {
|
||||
if dateStr == "" {
|
||||
*taskField = nil
|
||||
} else {
|
||||
parsed, err := engine.ParseDate(dateStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid date for '%s': %w", fieldName, err)
|
||||
}
|
||||
*taskField = &parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurrence
|
||||
if recurStr, ok := fields["recurrence"]; ok {
|
||||
if recurStr == "" {
|
||||
// Clear recurrence
|
||||
if task.ParentUUID != nil {
|
||||
// This is an instance - clear parent's recurrence
|
||||
parent, err := engine.GetTask(*task.ParentUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load parent task: %w", err)
|
||||
}
|
||||
parent.RecurrenceDuration = nil
|
||||
if err := parent.Save(); err != nil {
|
||||
return fmt.Errorf("failed to update parent recurrence: %w", err)
|
||||
}
|
||||
fmt.Println("Cleared recurrence pattern (no more instances will be spawned)")
|
||||
} else {
|
||||
task.RecurrenceDuration = nil
|
||||
}
|
||||
} else {
|
||||
// Parse recurrence
|
||||
duration, err := engine.ParseRecurrencePattern(recurStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid recurrence pattern: %w", err)
|
||||
}
|
||||
|
||||
if task.ParentUUID != nil {
|
||||
// This is an instance - update parent's recurrence
|
||||
parent, err := engine.GetTask(*task.ParentUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load parent task: %w", err)
|
||||
}
|
||||
parent.RecurrenceDuration = &duration
|
||||
if err := parent.Save(); err != nil {
|
||||
return fmt.Errorf("failed to update parent recurrence: %w", err)
|
||||
}
|
||||
fmt.Println("Updated recurrence pattern for future instances")
|
||||
} else {
|
||||
// This is a regular task or template
|
||||
task.RecurrenceDuration = &duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tags - replace all tags
|
||||
if tagsStr, ok := fields["tags"]; ok {
|
||||
// Remove all existing tags
|
||||
for _, tag := range task.Tags {
|
||||
if err := task.RemoveTag(tag); err != nil {
|
||||
return fmt.Errorf("failed to remove tag '%s': %w", tag, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add new tags
|
||||
if tagsStr != "" {
|
||||
newTags := parseTags(tagsStr)
|
||||
for _, tag := range newTags {
|
||||
if err := task.AddTag(tag); err != nil {
|
||||
return fmt.Errorf("failed to add tag '%s': %w", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEditor returns the editor command ($EDITOR or 'vi')
|
||||
func getEditor() string {
|
||||
if editor := os.Getenv("EDITOR"); editor != "" {
|
||||
return editor
|
||||
}
|
||||
return "vi"
|
||||
}
|
||||
|
||||
// launchEditor opens file in editor and waits
|
||||
func launchEditor(filepath string) error {
|
||||
editor := getEditor()
|
||||
|
||||
cmd := exec.Command(editor, filepath)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return fmt.Errorf("editor exited with error (code %d)", exitErr.ExitCode())
|
||||
}
|
||||
return fmt.Errorf("failed to run editor: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePriority converts H/M/L/D to Priority constant
|
||||
func parsePriority(s string) (engine.Priority, error) {
|
||||
switch strings.ToUpper(strings.TrimSpace(s)) {
|
||||
case "H":
|
||||
return engine.PriorityHigh, nil
|
||||
case "M":
|
||||
return engine.PriorityMedium, nil
|
||||
case "L":
|
||||
return engine.PriorityLow, nil
|
||||
case "D", "":
|
||||
return engine.PriorityDefault, nil
|
||||
default:
|
||||
return engine.PriorityDefault, fmt.Errorf("invalid priority '%s' (must be H, M, L, or D)", s)
|
||||
}
|
||||
}
|
||||
|
||||
// parseStatus converts string to Status constant
|
||||
func parseStatus(s string) (engine.Status, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||
case "pending":
|
||||
return engine.StatusPending, nil
|
||||
case "completed":
|
||||
return engine.StatusCompleted, nil
|
||||
case "deleted":
|
||||
return engine.StatusDeleted, nil
|
||||
case "recurring", "template":
|
||||
// Accept these but keep them unchanged (system-managed status)
|
||||
return engine.StatusRecurring, nil
|
||||
default:
|
||||
return engine.StatusPending, fmt.Errorf("invalid status '%s' (must be: pending, completed, deleted, or recurring)", s)
|
||||
}
|
||||
}
|
||||
|
||||
// parseTags splits comma-separated tags
|
||||
func parseTags(s string) []string {
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
parts := strings.Split(s, ",")
|
||||
tags := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
tag := strings.TrimSpace(part)
|
||||
if tag != "" {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
// formatTimeForEdit formats a time for the edit file
|
||||
func formatTimeForEdit(t time.Time) string {
|
||||
// Check if time component is at midnight (00:00)
|
||||
if t.Hour() == 0 && t.Minute() == 0 {
|
||||
// Date only, no time component
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
// Date with time - use colon format that parser understands
|
||||
return t.Format("2006-01-02:15:04")
|
||||
}
|
||||
|
||||
// formatStatusForEdit formats status for the edit file
|
||||
func formatStatusForEdit(s engine.Status) string {
|
||||
switch s {
|
||||
case engine.StatusPending:
|
||||
return "pending"
|
||||
case engine.StatusCompleted:
|
||||
return "completed"
|
||||
case engine.StatusDeleted:
|
||||
return "deleted"
|
||||
case engine.StatusRecurring:
|
||||
return "recurring"
|
||||
default:
|
||||
return "pending"
|
||||
}
|
||||
}
|
||||
|
||||
// formatPriorityForEdit formats priority for the edit file
|
||||
func formatPriorityForEdit(p engine.Priority) string {
|
||||
switch p {
|
||||
case engine.PriorityHigh:
|
||||
return "H"
|
||||
case engine.PriorityMedium:
|
||||
return "M"
|
||||
case engine.PriorityLow:
|
||||
return "L"
|
||||
case engine.PriorityDefault:
|
||||
return "D"
|
||||
default:
|
||||
return "D"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.jnss.me/joakim/opal/internal/engine"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var infoCmd = &cobra.Command{
|
||||
Use: "info [filter...]",
|
||||
Short: "Show detailed information about a task",
|
||||
Long: `Display all details about a specific task.
|
||||
|
||||
Examples:
|
||||
opal info 2 # Show details for task with display ID 2
|
||||
opal 2 info # Flexible syntax (same as above)
|
||||
opal info +urgent # Show details if only one task matches`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
parsed := getParsedArgs(cmd)
|
||||
if err := showTaskInfo(parsed.Filters); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func showTaskInfo(args []string) error {
|
||||
// Validate we have a filter
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("no task specified for info command")
|
||||
}
|
||||
|
||||
// Parse filter
|
||||
filter, err := engine.ParseFilter(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse filter: %w", err)
|
||||
}
|
||||
|
||||
// Load working set to resolve IDs
|
||||
ws, err := engine.LoadWorkingSet()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load working set: %w", err)
|
||||
}
|
||||
|
||||
// Resolve task
|
||||
var task *engine.Task
|
||||
|
||||
if len(filter.IDs) > 0 {
|
||||
// Resolve display ID (should be exactly one)
|
||||
if len(filter.IDs) != 1 {
|
||||
return fmt.Errorf("info requires exactly one task (specified %d IDs)", len(filter.IDs))
|
||||
}
|
||||
task, err = ws.GetTaskByDisplayID(filter.IDs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Use filter to get tasks
|
||||
tasks, err := engine.GetTasks(filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get tasks: %w", err)
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return fmt.Errorf("no task found matching filter")
|
||||
}
|
||||
if len(tasks) > 1 {
|
||||
return fmt.Errorf("info requires exactly one task (filter matched %d tasks)", len(tasks))
|
||||
}
|
||||
|
||||
task = tasks[0]
|
||||
}
|
||||
|
||||
// Display detailed info
|
||||
fmt.Println(engine.FormatTaskDetail(task))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const parsedArgsKey contextKey = "parsedArgs"
|
||||
var commandNames = []string{
|
||||
"add", "list", "done", "modify", "delete",
|
||||
"start", "stop", "count", "projects", "tags",
|
||||
"info", "edit",
|
||||
}
|
||||
|
||||
var commandsWithModifiers = map[string]bool{
|
||||
@@ -169,6 +170,8 @@ func init() {
|
||||
rootCmd.AddCommand(countCmd)
|
||||
rootCmd.AddCommand(projectsCmd)
|
||||
rootCmd.AddCommand(tagsCmd)
|
||||
rootCmd.AddCommand(infoCmd)
|
||||
rootCmd.AddCommand(editCmd)
|
||||
}
|
||||
|
||||
func initializeApp() {
|
||||
|
||||
Reference in New Issue
Block a user