aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod3
-rw-r--r--main.go237
2 files changed, 240 insertions, 0 deletions
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..16e4d40
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module ws
+
+go 1.23.1
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..6df83ff
--- /dev/null
+++ b/main.go
@@ -0,0 +1,237 @@
+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
+// - 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
+// - import: read multiple lines from stdin and import them as taks
+
+import (
+ "bufio"
+ "encoding/gob"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strconv"
+ "time"
+)
+
+var (
+ // Stack of active tasks
+ Tasks []string
+ // Completede tasks
+ TasksDone []TaskDone
+ // Persistent storage for Tasks
+ gobdata string = "tasks.gob"
+ DateLayout string = "15:04:05 02/01/2006"
+)
+
+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)
+}
+
+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)
+}
+
+// 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(Tasks) > n-1 && n >= 1) {
+ fmt.Println("N out of range.")
+ os.Exit(3)
+ }
+ n -= 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(&TasksDone)
+ if err != nil {
+ panic(err)
+ }
+ err = dec.Decode(&Tasks)
+ if err != nil {
+ panic(err)
+ }
+ }
+ }
+
+ if len(os.Args) == 1 {
+ // default
+ if len(Tasks) == 0 {
+ fmt.Println("No tasks.")
+ return
+ }
+ fmt.Println("1.", 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)
+ }
+
+ // remove newline
+ t = t[:len(t)-1]
+
+ Tasks = append(Tasks, t)
+
+ case "undone":
+
+ case "del":
+ n := NArg(3)
+ if len(Tasks) == 1 {
+ Tasks = []string{}
+ return
+ }
+ Tasks = append(Tasks[:n], Tasks[n+1:]...)
+
+ case "done":
+ n := NArg(3)
+ TasksDone = append(TasksDone, TaskDone{
+ Text: Tasks[n],
+ Date: time.Now(),
+ })
+ if len(Tasks) == 1 {
+ Tasks = []string{}
+ return
+ }
+ Tasks = append(Tasks[:n], Tasks[n+1:]...)
+
+ 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])
+ }
+
+ 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)
+ }
+ }
+ // 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
+ }
+
+ var n int = NArg(3)
+
+ t := Tasks[n]
+
+ Tasks = append(Tasks[:n], Tasks[n+1:]...)
+ Tasks = append(Tasks, t)
+
+ default:
+ usage()
+ }
+
+ // Save data to gobdata
+ f, err = os.Create(gobdata)
+ if err != nil {
+ panic(err)
+ }
+ enc := gob.NewEncoder(f)
+ err = enc.Encode(TasksDone)
+ if err != nil {
+ panic(err)
+ }
+ err = enc.Encode(Tasks)
+ if err != nil {
+ panic(err)
+ }
+}