aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaymaekers Luca <raymaekers.luca@gmail.com>2024-10-10 11:02:37 +0200
committerRaymaekers Luca <raymaekers.luca@gmail.com>2024-10-11 18:21:08 +0200
commit98dd9ae0629d58b16603d0188bd7e6f6f182e337 (patch)
tree0de107af73fd8e1f929b471de0f0b3154e0a01cb
parent5834e02b741a4ee4d2b2bd88f0abc9eeed725a5d (diff)
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
-rw-r--r--.gitignore1
-rw-r--r--LICENSE19
-rw-r--r--main.go320
3 files changed, 234 insertions, 106 deletions
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
+// - [ ] <no arg>
+// - [ ] 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 <arg> 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 <command>
+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)
}
+
}