package main import ( "encoding/gob" "errors" "fmt" "os" "strconv" "strings" "time" ws "git.spacehb.net/ws.git" ) var ( // Stack of active tasks Tags []ws.Tag // Active tasks Tasks []ws.Task // Completed tasks TasksDone []ws.TaskDone // Persistent storage for Tasks ) // Search for arg in Args and return the index at which it was found // Returns -1 if it was not found func ArgInArgs(args []string, arg string) int { for i := 0; i < len(args); i++ { if args[i] == arg { return i } } return -1 } // Parse a number argument from os.Args where pos is the argument's position // returns the int value for the string, if it fails the program will exit. // def will be used as default value when there is no argument at the position. If you do not want // to pass a default value, you can pass -1. func ParseNArg(pos, def, max int) int { if max == 0 { fmt.Printf("Number out of range: %d\n", 0) os.Exit(1) } if len(os.Args) == pos { if def == -1 { fmt.Println("Argument required: N") os.Exit(1) } return def } n, err := strconv.Atoi(os.Args[pos]) if errors.Is(err, strconv.ErrSyntax) { fmt.Printf("'%s' is not a number.\n", os.Args[pos]) os.Exit(1) } else if err != nil { panic(err) } if n > max { fmt.Printf("Number out of range: %d\n", n) os.Exit(1) } return n } func main() { var ( dec *gob.Decoder f *os.File err error gobdataPath string = ws.GetGobdataPath() ) // Open/Create gob data file if not exist { f, err = os.Open(gobdataPath) if errors.Is(err, os.ErrNotExist) { // Do nothing } else if err != nil { panic(err) } else { dec = gob.NewDecoder(f) err = dec.Decode(&Tasks) if err != nil { panic(err) } err = dec.Decode(&TasksDone) if err != nil { panic(err) } err = dec.Decode(&Tags) if err != nil { panic(err) } } } // When no arguments are provided display the first task. if len(os.Args) == 1 { if len(Tasks) == 0 { fmt.Println("No tasks.") return } fmt.Printf("1. %s\n", Tasks[0]) os.Exit(0) } switch os.Args[1] { // Add a new task case "task": var tagName, taskText string var i, offset int // offset of 2 because we are at the second arg offset = 2 i = ArgInArgs(os.Args[offset:], "-t") // tag argument was provided if i != -1 { if offset+i+1 >= len(os.Args) { fmt.Println("-t requires an argument.") os.Exit(1) } tagName = os.Args[offset+i+1] if tagName == "" { fmt.Println("-t requires an argument.") os.Exit(1) } // this would mean that -t are the first and last two arguments if len(os.Args) == offset+2 { fmt.Println("Task text is required.") os.Exit(1) } if i == 0 { // tag is at the start taskText = strings.Join(os.Args[offset+i+2:], " ") } else if i+4 == len(os.Args) { // tag is at the end taskText = strings.Join(os.Args[offset:i+2], " ") } else { // tag is in the middle taskText = strings.Join(append(os.Args[offset:i+2], os.Args[offset+i+2:]...), " ") } } else { taskText = strings.Join(os.Args[offset:], " ") } // taskText can be provided as "" which is an argument and will pass previous validation // tests if taskText == "" { fmt.Println("Task text is required.") os.Exit(1) } if tagName == "" { Tasks = append(Tasks, ws.Task{Text: taskText}) break } tag := ws.Tag(tagName) // Validate tag found := false for _, v := range Tags { if tag == v { found = true break } } if !found { fmt.Printf("No tag '%s' found.\n", tagName) os.Exit(1) } Tasks = append(Tasks, ws.Task{Text: taskText, Tag: tag}) // Delete an active task case "del": n := ParseNArg(2, 1, len(Tasks)) Tasks = append(Tasks[:n-1], Tasks[n:]...) // Mark an active task as done case "done": n := ParseNArg(2, 1, len(Tasks)) TasksDone = append(TasksDone, ws.TaskDone{ Task: Tasks[n-1], Date: time.Now(), }) Tasks = append(Tasks[:n-1], Tasks[n:]...) // Undo a done task case "undone": n := ParseNArg(2, 1, len(TasksDone)) Tasks = append(Tasks, TasksDone[n-1].Task) TasksDone = append(TasksDone[:n-1], TasksDone[n:]...) // Procrastinate an active task case "pc": n := ParseNArg(2, 1, len(Tasks)) if n == 1 { Tasks = append(Tasks[n:], Tasks[n]) break } // save, delete and append task t := Tasks[n-1] Tasks = append(Tasks[:n-1], Tasks[n:]...) Tasks = append(Tasks, t) // Short list of active tasks case "ls": if len(Tasks) == 0 { fmt.Println("No tasks.") break } for i := 0; i < len(Tasks) && i < ws.TASK_LIST_COUNT; i++ { fmt.Printf("%d. %s\n", i+1, Tasks[i]) } // List all active and done tasks case "list": if len(Tasks) > 0 { fmt.Println("Active:") for i, t := range Tasks { fmt.Printf("% 2d. %s\n", i+1, t) } } if len(TasksDone) > 0 { fmt.Println("Done:") for i, t := range TasksDone { fmt.Printf("% 2d. %s\n", i+1, t) } } // create a new tag case "tag": tagName := ws.Tag(strings.Join(os.Args[2:], " ")) for i := 0; i < len(Tags); i++ { if tagName == Tags[i] { fmt.Printf("Tag '%s' already exists.\n", tagName) os.Exit(1) } } Tags = append(Tags, tagName) // delete a tag case "tagd": n := ParseNArg(2, 1, len(Tags)) Tags = append(Tags[:n-1], Tags[n:]...) // list tags case "tagl": for i := 0; i < len(Tags); i++ { fmt.Printf("%2d. %s\n", i+1, Tags[i]) } default: fmt.Println(`usage: ws COMMANDS task Add a new task done Mark an active task as done undone Undo a done task del Delete an active task pc Procrastinate an active task ls Short list of active tasks list List all active and done tasks tag Add a new tag tagd Delete a tag tagl List tags`) } // Save data to gobdata f, err = os.Create(gobdataPath) if err != nil { panic(err) } enc := gob.NewEncoder(f) err = enc.Encode(Tasks) if err != nil { panic(err) } err = enc.Encode(TasksDone) if err != nil { panic(err) } err = enc.Encode(Tags) if err != nil { panic(err) } }