From b0397503c8bdcc98216bab220b661466c74a63e9 Mon Sep 17 00:00:00 2001 From: Raymaekers Luca Date: Sat, 12 Oct 2024 13:05:02 +0200 Subject: Create separate module for primitives - Added a converter tool to convert from an old version of gobdata to a new format - added ws sub directory for the command line tool --- converter/main.go | 127 ++++++++++++++++++++ go.mod | 2 +- main.go | 345 ------------------------------------------------------ workstack.go | 76 ++++++++++++ ws/main.go | 291 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 495 insertions(+), 346 deletions(-) create mode 100644 converter/main.go delete mode 100644 main.go create mode 100644 workstack.go create mode 100644 ws/main.go diff --git a/converter/main.go b/converter/main.go new file mode 100644 index 0000000..a863b2c --- /dev/null +++ b/converter/main.go @@ -0,0 +1,127 @@ +/* +Sample code to convert an old tasks.gob file to a newer format +This is scrpited and dangerous, keep backups!!! +*/ +package main + +import ( + "encoding/gob" + "fmt" + "os" + "time" + + ws "git.spacehb.net/ws.git" +) + +func main() { + + TasksDoneConv := []ws.TaskDone{} + TasksConv := []ws.Task{} + TagsConv := []ws.Tag{} + + // Get old gobdata + f, err := os.Open("tasks.gob") + if err != nil { + panic(err) + } + + dec := gob.NewDecoder(f) + // old data format + Tasks := []string{} + TasksDone := []struct { + Text string + Date time.Time + }{} + err = dec.Decode(&TasksDone) + if err != nil { + panic(err) + } + err = dec.Decode(&Tasks) + if err != nil { + panic(err) + } + + // Get current gobdata + f, err = os.Open(ws.GetGobdataPath()) + if err != nil { + panic(err) + } + dec = gob.NewDecoder(f) + err = dec.Decode(&TasksConv) + if err != nil { + panic(err) + } + err = dec.Decode(&TasksDoneConv) + if err != nil { + panic(err) + } + err = dec.Decode(&TagsConv) + if err != nil { + panic(err) + } + + for _, v := range Tasks { + start := false + var tag []byte + var text []byte + for i := 0; i < len(v); i++ { + if v[i] == '{' { + start = true + i++ + } + if v[i] == '}' { + start = false + i++ + } + if start == true { + tag = append(tag, v[i]) + } + if start == false { + text = append(text, v[i]) + } + } + + tagstr := ws.Tag(tag) + if len(TagsConv) == 0 { + TagsConv = append(TagsConv, tagstr) + } + found := false + for _, v := range TagsConv { + if tagstr == v { + found = true + break + } + } + if !found { + TagsConv = append(TagsConv, ws.Tag(tag)) + } + + textstr := string(text[1:]) + + TasksConv = append(TasksConv, ws.Task{Text: textstr, Tag: tagstr}) + // fmt.Printf("tag: '%s' | text: '%s'\n", tag, text) + } + fmt.Printf("TagsConv: %#v\n", TagsConv) + for i, v := range TasksConv { + fmt.Printf("%2d. %s\n", i+1, v) + } + + // Save data to gobdata + f, err = os.Create(ws.GetGobdataPath()) + if err != nil { + panic(err) + } + enc := gob.NewEncoder(f) + err = enc.Encode(TasksConv) + if err != nil { + panic(err) + } + err = enc.Encode(TasksDoneConv) + if err != nil { + panic(err) + } + err = enc.Encode(TagsConv) + if err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index 16e4d40..2b3dc0d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module ws +module git.spacehb.net/ws.git go 1.23.1 diff --git a/main.go b/main.go deleted file mode 100644 index bc6ac84..0000000 --- a/main.go +++ /dev/null @@ -1,345 +0,0 @@ -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) - } - -} diff --git a/workstack.go b/workstack.go new file mode 100644 index 0000000..1205039 --- /dev/null +++ b/workstack.go @@ -0,0 +1,76 @@ +package workstack + +// 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 ( + "fmt" + "os" + "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 Tag string + +type Task struct { + Text string + Tag Tag +} + +func (t Task) String() string { + if t.Tag != "" { + return fmt.Sprintf("{%s} %s", t.Tag, t.Text) + } else { + return fmt.Sprintf("%s", t.Text) + } +} + +var ( + // Persistent storage for Tasks + Gobdata string = "tasks.gob" + DateLayout string = "15:04:05 02/01/2006" +) + +const ( + TASK_LIST_COUNT = 5 + GOBDATA_FILENAME = "tasks.gob" +) + +func GetGobdataPath() string { + p := os.Getenv("HOME") + if p == "" { + panic("HOME var not set.") + } + return p + "/sync/share/" + GOBDATA_FILENAME +} diff --git a/ws/main.go b/ws/main.go new file mode 100644 index 0000000..09a4731 --- /dev/null +++ b/ws/main.go @@ -0,0 +1,291 @@ +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 { + 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, 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) + } + +} -- cgit v1.2.3