From 98dd9ae0629d58b16603d0188bd7e6f6f182e337 Mon Sep 17 00:00:00 2001 From: Raymaekers Luca Date: Thu, 10 Oct 2024 11:02:37 +0200 Subject: Add tags to tasks Adds tag functionality to tasks - Added Task type - Added Tag type - Save Tags to Gobdata - Renamed gobdata -> Gobdata - Replaced NArg by ParseNArg function - Added ArgInArgs helper function - Added testing to TODO's - Refactored switch statement - Removed usage function - Added gitignore - Added LICENSE --- .gitignore | 1 + LICENSE | 19 ++++ main.go | 320 +++++++++++++++++++++++++++++++++++++++++-------------------- 3 files changed, 234 insertions(+), 106 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..33de139 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +zlib License + +Copyright (C) 2024 Luca Raymaekers + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/main.go b/main.go index 6df83ff..bc6ac84 100644 --- a/main.go +++ b/main.go @@ -3,35 +3,70 @@ 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]". +// 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 -// - tags -// - command to add/delete tags so that we can programatically add them -// - parameter to add to specify a tag could be a format in the string or -flag -// - edit: replace a Task's text +// - 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 ( - "bufio" "encoding/gob" "errors" "fmt" - "io" "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 - Tasks []string - // Completede tasks + Tags []string + // Active tasks + Tasks []Task + // Completed tasks TasksDone []TaskDone // Persistent storage for Tasks - gobdata string = "tasks.gob" + Gobdata string = "tasks.gob" DateLayout string = "15:04:05 02/01/2006" ) @@ -39,50 +74,46 @@ const ( TASK_LIST_COUNT = 5 ) -type TaskDone struct { - Text string - Date time.Time -} - -func (t TaskDone) String() string { - return fmt.Sprintf("(%s) %s", t.Date.Format(DateLayout), t.Text) +// 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 } -func usage() { - fmt.Println(`usage: ws [add|done [N]|pc [N]|ls|list|reset] -add Adds a task to the end of the stack -done [N] Mark a task as done -undone [N] Mask a task as undone -pc [N] Procrastinate task a task to end of stack -del [N] Delete an active task -ls List active tasks -list List all tasks -reset Recreate the tasks.gob file - -tasks.gob: Where the tasks are saved, the default path is $HOME/sync/share/tasks.gob -N: When N is not provided the default is 1`) - os.Exit(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) + } -// Helper function for parsing an int argument to a command, takes as parameter the current argument -// count and parses os.Args to return an int. -// If there is no argument provided returns 0 else it returns the argument provided on the command -// line. -func NArg(c int) int { - var n int - var err error - if len(os.Args) < c { // no arguments - n = 0 - } else { - n, err = strconv.Atoi(os.Args[2]) - if err != nil { - panic(err) + if len(os.Args) == pos { + if def == -1 { + fmt.Println("Argument required: N") + os.Exit(1) } - if !(len(Tasks) > n-1 && n >= 1) { - fmt.Println("N out of range.") - os.Exit(3) - } - n -= 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 } @@ -99,81 +130,139 @@ func main() { if p == "" { panic("HOME var not set.") } - gobdata = p + "/sync/share/" + gobdata + Gobdata = p + "/sync/share/" + Gobdata - f, err = os.Open(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(&Tasks) + err = dec.Decode(&Tags) if err != nil { panic(err) } } } + // When no arguments are provided display the first task. if len(os.Args) == 1 { - // default if len(Tasks) == 0 { fmt.Println("No tasks.") return } - fmt.Println("1.", Tasks[0]) + fmt.Printf("1. %s\n", Tasks[0]) os.Exit(0) } + switch os.Args[1] { - case "add": - // prompt - fmt.Fprint(os.Stderr, ">") - - // read console input - reader := bufio.NewReader(os.Stdin) - t, err := reader.ReadString('\n') - if err == io.EOF { - fmt.Fprint(os.Stderr, "\n") - os.Exit(0) - } - if err != nil { - panic(err) - } - if len(t) == 1 { - os.Exit(0) + // 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:], " ") } - // remove newline - t = t[:len(t)-1] + // 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) + } - Tasks = append(Tasks, t) + if tagName == "" { + Tasks = append(Tasks, Task{Text: taskText}) + break + } - case "undone": + // 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 := NArg(3) - if len(Tasks) == 1 { - Tasks = []string{} - return - } - Tasks = append(Tasks[:n], Tasks[n+1:]...) + n := ParseNArg(2, 1, len(Tasks)) + Tasks = append(Tasks[:n-1], Tasks[n:]...) + // Mark an active task as done case "done": - n := NArg(3) + n := ParseNArg(2, 1, len(Tasks)) TasksDone = append(TasksDone, TaskDone{ - Text: Tasks[n], + Task: Tasks[n-1], Date: time.Now(), }) - if len(Tasks) == 1 { - Tasks = []string{} - return + 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 } - Tasks = append(Tasks[:n], Tasks[n+1:]...) + // 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.") @@ -183,6 +272,7 @@ func main() { fmt.Printf("%d. %s\n", i+1, Tasks[i]) } + // List all active and done tasks case "list": if len(Tasks) > 0 { fmt.Println("Active:") @@ -196,42 +286,60 @@ func main() { fmt.Printf("% 2d. %s\n", i+1, t) } } - // fmt.Println(Tasks, TasksDone) - - case "reset": - Tasks = []string{} - TasksDone = []TaskDone{} - case "pc": //procrastinate - // procrastinate top task by default - if len(os.Args) < 3 { - Tasks = append(Tasks[1:], Tasks[0]) - break + // 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) - var n int = NArg(3) - - t := Tasks[n] - - Tasks = append(Tasks[:n], Tasks[n+1:]...) - Tasks = append(Tasks, t) + // 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: - usage() + 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) + 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(Tasks) + err = enc.Encode(Tags) if err != nil { panic(err) } + } -- cgit v1.2.3