package main // Workstack or ws for short is a program that manages To-Do's in a stack-based fashion. It tries // to guide your focus to your three most important tasks such that you do not get distracted by // other tasks. // Every task added starts as inactive "[ ]" and can be marked as done by changing the status to // "[x]". // When the programs exits Tasks are saved to a tasks.gob file, this will truncate (os.Create) the // existing file. // TODO's // - edit functionality // - import: read multiple lines from stdin and import them as taks // - parsing text as Tasks, maybe helper program? // - clocking functionality with a 'task' command // - testing: // - [ ] add // - [ ] done // - [ ] undone // - [ ] del // - [ ] pc // - [ ] // - [ ] ls // - [ ] list // - [ ] tag // - [ ] tagd // - [ ] tagl import ( "encoding/gob" "errors" "fmt" "os" "strconv" "strings" "time" ) type TaskDone struct { Task Task Date time.Time } func (t TaskDone) String() string { return fmt.Sprintf("(%s) %s", t.Date.Format(DateLayout), t.Task) } type Task struct { Text string Tag string } func (t Task) String() string { if t.Tag != "" { return fmt.Sprintf("%s {%s}", t.Text, t.Tag) } else { return fmt.Sprintf("%s", t.Text) } } var ( // Stack of active tasks Tags []string // Active tasks Tasks []Task // Completed tasks TasksDone []TaskDone // Persistent storage for Tasks Gobdata string = "tasks.gob" DateLayout string = "15:04:05 02/01/2006" ) const ( TASK_LIST_COUNT = 5 ) // 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() { // create tasks.gob file var dec *gob.Decoder var f *os.File var err error // Open/Create gob data file if not exist { p := os.Getenv("HOME") if p == "" { panic("HOME var not set.") } Gobdata = p + "/sync/share/" + Gobdata f, err = os.Open(Gobdata) 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 { fmt.Println(1) // 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, Task{Text: taskText}) break } // Validate tag found := false for _, v := range Tags { if tagName == v { found = true break } } if !found { fmt.Printf("No tag '%s' found.\n", tagName) os.Exit(1) } Tasks = append(Tasks, Task{Text: taskText, Tag: tagName}) // 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, 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 < 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 := 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(Gobdata) 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) } }