hlfw.ca

todo2

Download patch

ref: 4b410a5ffd714421418b90186dede34fae8e8f7e
parent: 7086ce5e2ff568f407c4d9c35f2057bbbbe0d2c1
author: halfwit <michaelmisch1985@gmail.com>
date: Fri Nov 29 01:44:08 PST 2019

Sweeping changes to finish off everything but the list and generate features

--- a/command.go
+++ b/command.go
@@ -38,13 +38,13 @@
 		if flag.NArg() < 2 {
 			return errors.New("No arguments supplied to rm")
 		}
-		c.args = flag.Args()[:1]
+		c.args = flag.Args()[1:]
 		c.runner = rm
 	case "add":
 		if flag.NArg() < 2 {
 			return errors.New("No arguments supplied to add")
 		}
-		c.args = flag.Args()[:1]
+		c.args = flag.Args()[1:]
 		c.runner = add
 	case "generate":
 		c.runner = generate
--- a/generate.go
+++ b/generate.go
@@ -1,6 +1,27 @@
 package main
 
-// TODO(halfwit): [#1] Create generate functions
-// [ ] WalkFunc for any match
-// [ ] Create each file
-// [ ] Make sure file doesn't already exist
+// WalkFunc through and search each file. We do want to early exit on an unsupported mime
+// We'll use a codified list of mappings for now, PR to update.
+// use http.DetectContentType(data) and send off to a go routine when it's a known type
+// Return on channel to run l.command on for the given types, run commands in main thread until done
+// In the end, we'll have a workable layout
+
+type generator struct {
+}
+
+func newGenerator() *generator {
+	return nil
+}
+
+func (g *generator) dotTodoExists() bool {
+	return false
+}
+
+func (g *generator) parseTodo() {
+
+}
+
+// In our scrub function we need tags, so if there isn't one in a TODO entry, etc we will assume `[general]`
+func (g *generator) toLayout() *Layout {
+	return nil
+}
--- a/layout.go
+++ b/layout.go
@@ -6,59 +6,38 @@
 	"fmt"
 	"log"
 	"os"
+	"regexp"
+	"strings"
 )
 
-/* This is what we want after parsing
+// Layout represents a fully parsed .todo file
 type Layout struct {
-	TaskList []*Tasks
+	Jobs []*Job
 }
 
-type Tasks struct {
-	Tag string
+// Job - A tagged collection of tasks
+type Job struct {
+	Tags     []string
 	Requires []string
-	Tasks []*Task
+	Tasks    []*Task
 }
 
+// Task - A collection of Entries (checkboxes)
 type Task struct {
-	Title string
+	Title   string
 	Entries []*Entry
 }
 
+// Entry -  single checkbox todo item
 type Entry struct {
 	Desc string
 	Done bool
 }
-*/
 
-const (
-	parent int = iota
-	child
-)
-
-// Layout - Structure representing a given .todo file
-type Layout struct {
-	TaskList []*Entries
-}
-
-// Entries - set of todo items for a give task
-type Entries struct {
-	Title string
-	List  []*Entry
-}
-
-// Entry - individual item of a set of tasks
-type Entry struct {
-	Desc string
-	Done bool
-}
-
-func layoutFromWorkTree() (*Layout, error) {
-	return nil, nil
-}
-
 func layoutFromTodoFile() (*Layout, error) {
+
 	l := &Layout{
-		TaskList: []*Entries{},
+		Jobs: []*Job{},
 	}
 	fl, err := os.Open(".todo")
 	if err != nil {
@@ -66,25 +45,48 @@
 	}
 	sc := bufio.NewScanner(fl)
 	for sc.Scan() {
-		if sc.Text() == "" {
+		txt := sc.Text()
+		if txt == "" {
 			continue
 		}
-
-		tl := &Entries{
-			Title: sc.Text(),
-			List:  parseEntries(sc),
+		// Build out entries in a stepwise-fashion. A proper parser grammer would be better here
+		// but this is quick and assumes machine-written input
+		j := &Job{}
+		j.Tags = parseTags(txt)
+		j.Requires = parseRequires(txt)
+		sc.Scan()
+		t := &Task{}
+		txt = sc.Text()
+		if txt == "" {
+			continue
 		}
-		l.TaskList = append(l.TaskList, tl)
+		if txt[:1] != "[" {
+			t.Title = txt
+			sc.Scan()
+		}
+		t.Entries = findEntries(sc)
+		j.Tasks = append(j.Tasks, t)
+		l.Jobs = append(l.Jobs, j)
 	}
 	if err := sc.Err(); err != nil {
 		return nil, err
 	}
-
 	return l, nil
 }
 
-func parseEntries(sc *bufio.Scanner) []*Entry {
-	sc.Scan()
+func parseTags(txt string) []string {
+	n := strings.Index(txt, ":")
+	return stringToTags(txt[:n])
+}
+
+func parseRequires(txt string) []string {
+	n := strings.Index(txt, ": releases ")
+	if len(txt) > n+len(": releases [ ]") {
+		return stringToTags(txt[n+len(": releases "):])
+	}
+	return nil
+}
+func findEntries(sc *bufio.Scanner) []*Entry {
 	en := []*Entry{}
 	for {
 		d := sc.Text()
@@ -112,42 +114,124 @@
 	}
 }
 
+func contains(first []string, last string) bool {
+	for _, f := range first {
+		if f == last {
+			return true
+		}
+	}
+	return false
+}
+
+func tagsMatch(first, last []string) bool {
+	if len(first) != len(last) {
+		return false
+	}
+
+	for n, f := range first {
+		if f != last[n] {
+			return false
+		}
+	}
+	return true
+}
+
+// This is adequate for our needs.
+func stringToJobs(incoming string) (*Job, error) {
+	r := regexp.MustCompile(`([^\[\]]*)\s?(\[.*\])+\s?(.*)`)
+	matches := r.FindAllStringSubmatch(incoming, -1)
+	if len(matches[0]) < 3 {
+		return nil, errors.New("Could not parse string")
+	}
+	title := matches[0][1]
+	if len(matches[0]) == 4 {
+		title = fmt.Sprintf("%s%s", matches[0][1], matches[0][3])
+	}
+	t := &Task{
+		Title: title,
+	}
+	return &Job{
+		Tags:  stringToTags(matches[0][2]),
+		Tasks: []*Task{t},
+	}, nil
+}
+
+func trimBrackets(incoming string) string {
+	incoming = strings.TrimPrefix(incoming, "[")
+	return strings.TrimSuffix(incoming, "]")
+}
+
+func stringToTags(incoming string) []string {
+	var tags []string
+	var tmp string
+	var startTag bool
+	n := 0
+	for _, b := range []byte(incoming) {
+		if b == '[' {
+			startTag = true
+			tmp = ""
+			continue
+		}
+		if b == ']' {
+			n++
+			tags = append(tags, tmp)
+			startTag = false
+		}
+		if startTag {
+			tmp += string(b)
+		}
+	}
+	if tags[0] == "" {
+		return nil
+	}
+	return tags
+}
+
 func (l *Layout) destroy(title string) error {
 	if _, err := os.Stat(".todo"); err != nil {
 		return errors.New("Unable to locate .todo file")
 	}
-	for i, e := range l.TaskList {
-		if e.Title != title {
+	tasks, err := stringToJobs(title)
+	if err != nil {
+		return err
+	}
+	for i, e := range l.Jobs {
+		if tagsMatch(e.Tags, tasks.Tags) {
 			continue
 		}
-		if i < len(l.TaskList)-1 {
-			copy(l.TaskList[i:], l.TaskList[i+1:])
+		if i < len(l.Jobs)-1 {
+			copy(l.Jobs[i:], l.Jobs[i+1:])
 		}
-		l.TaskList = l.TaskList[:len(l.TaskList)-1]
+		l.Jobs = l.Jobs[:len(l.Jobs)-1]
 		return nil
+
 	}
 	return errors.New("No such Entry")
 }
 
-func (l *Layout) create(title string) error {
-	if len(title) < 1 {
-		return errors.New("Unable to add nil Entry")
+func (l *Layout) create(incoming string) error {
+	tasks, err := stringToJobs(incoming)
+	if err != nil {
+		return err
 	}
-	l.TaskList = append(l.TaskList, &Entries{
-		Title: title,
-		List:  []*Entry{},
-	})
+	l.Jobs = append(l.Jobs, tasks)
 	return nil
 }
 
-func (l *Layout) taskExists(Title, item string) bool {
-	for _, e := range l.TaskList {
-		if e.Title != Title {
+func (l *Layout) taskExists(title, item string) bool {
+	t, err := stringToJobs(title)
+	if err != nil {
+		return false
+	}
+	for _, e := range l.Jobs {
+		if !tagsMatch(e.Tags, t.Tags) {
 			continue
 		}
-		for _, t := range e.List {
-			if t.Desc == item {
-				return true
+		for _, t := range e.Tasks {
+			for _, d := range t.Entries {
+				if d.Desc == item {
+					return true
+				}
 			}
 		}
 	}
@@ -154,101 +238,117 @@
 	return false
 }
 
-func (l *Layout) addTask(Title, item string) error {
-	for _, e := range l.TaskList {
-		if e.Title != Title {
-			continue
-		}
-		e.List = append(e.List, &Entry{
-			Desc: item,
-			Done: false,
-		})
-		return nil
+// TODO(halfwit) Verify that task title doesn't already exist!
+func (l *Layout) addTask(title, item string) error {
+	t, err := stringToJobs(title)
+	if err != nil {
+		return err
 	}
-	line := &Entry{
+	entry := &Entry{
 		Desc: item,
 		Done: false,
 	}
-	l.TaskList = append(l.TaskList, &Entries{
-		Title: Title,
-		List:  []*Entry{line},
-	})
+	for _, job := range l.Jobs {
+		if !tagsMatch(job.Tags, t.Tags) {
+			continue
+		}
+		for _, task := range job.Tasks {
+			if task.Title == t.Tasks[0].Title {
+				task.Entries = append(task.Entries, entry)
+				return nil
+			}
+		}
+	}
+	t.Tasks[0].Entries = append(t.Tasks[0].Entries, entry)
+	for _, job := range l.Jobs {
+		if !tagsMatch(job.Tags, t.Tags) {
+			continue
+		}
+		job.Tasks = append(job.Tasks, t.Tasks[0])
+		return nil
+	}
+	l.Jobs = append(l.Jobs, t)
 	return nil
 }
 
-func (l *Layout) rmTask(Title, item string) error {
-	for _, e := range l.TaskList {
-		if e.Title != Title {
+func (l *Layout) rmTask(title, item string) error {
+	t, err := stringToJobs(title)
+	if err != nil {
+		return err
+	}
+	for _, e := range l.Jobs {
+		if !tagsMatch(e.Tags, t.Tags) {
 			continue
 		}
-		for i, t := range e.List {
-			if t.Desc != item {
-				continue
+		for _, t := range e.Tasks {
+			for i, j := range t.Entries {
+				if j.Desc != item {
+					continue
+				}
+				if i < len(t.Entries)-1 {
+					copy(t.Entries[i:], t.Entries[i+1:])
+				}
+				t.Entries = t.Entries[:len(t.Entries)-1]
+				return nil
 			}
-			if i < len(e.List)-1 {
-				copy(e.List[i:], e.List[i+1:])
-			}
-			e.List = e.List[:len(e.List)-1]
-			return nil
 		}
 	}
 	return fmt.Errorf("No such task/Entry")
 }
 
-func (l *Layout) toggleTask(Title, item string) error {
-	for _, e := range l.TaskList {
-		if e.Title != Title {
+func (l *Layout) toggleTask(title, item string) error {
+	t, err := stringToJobs(title)
+	if err != nil {
+		return err
+	}
+	for _, e := range l.Jobs {
+		if !tagsMatch(e.Tags, t.Tags) {
 			continue
 		}
-		for _, t := range e.List {
-			if t.Desc != item {
-				continue
+		for _, t := range e.Tasks {
+			for _, j := range t.Entries {
+				if j.Desc != item {
+					continue
+				}
+				j.Done = !j.Done
+				return nil
 			}
-			t.Done = !t.Done
-			return nil
 		}
 	}
 	return fmt.Errorf("No such task/Entry")
 }
 
-func (l *Layout) addLink(n int, from, to string) {
-	//for _, tasks := range l.TaskList {
-	switch n {
-	case parent:
-		//if tasks.Tag == to {
-		//	tasks.Requires = append(tasks.Requires, from)
-		//}
-	case child:
-		//if tasks.Tag == from {
-		//	tasks.Requires = append(tasks.Requires, to)
-		//}
+func (l *Layout) addLink(to, from string) {
+	to = trimBrackets(to)
+	from = trimBrackets(from)
+	for _, tasks := range l.Jobs {
+		for _, tag := range tasks.Tags {
+			if tag != to {
+				continue
+			}
+			if contains(tasks.Requires, from) {
+				continue
+			}
+			tasks.Requires = append(tasks.Requires, from)
+		}
 	}
-	//}
 }
 
-func (l *Layout) rmLink(n int, from, to string) {
-	//for _, tasks := range l.TaskList {
-	switch n {
-	case parent:
-		//if tasks.Tag == to {
-		//	for n, req := range tasks.Required {
-		//		if req != from {
-		//			continue
-		//      }
-		//		tasks.Required[n] = tasks.Required[len(tasks.Required)-1]
-		//		tasks.Required = tasks.Required[:len(tasks.Required)-1]
-		//	}
-		//}
-	case child:
-		//if tasks.Tag == from {
-		//	for n, req := range tasks.Required {
-		//		if req != to {
-		//			continue
-		//      }
-		//		tasks.Required[n] = tasks.Required[len(tasks.Required)-1]
-		//		tasks.Required = tasks.Required[:len(tasks.Required)-1]
-		//	}
-		//}
+// It's ugly, but it gets the thing done
+func (l *Layout) rmLink(to, from string) {
+	for _, tasks := range l.Jobs {
+		for _, tag := range tasks.Tags {
+			if tag != to {
+				continue
+			}
+			for n, req := range tasks.Requires {
+				if req != from {
+					continue
+				}
+				tasks.Requires[n] = tasks.Requires[len(tasks.Requires)-1]
+				tasks.Requires = tasks.Requires[:len(tasks.Requires)-1]
+			}
+
+		}
 	}
-	//}
 }
--- a/runners.go
+++ b/runners.go
@@ -7,9 +7,11 @@
 
 func list(c *command) error {
 	l, err := layoutFromTodoFile()
-	if err != nil && c.args[0] != "create" {
+	if err != nil {
 		return err
 	}
+	//d := dagFromLayout(l)
+	//writeList(d)
 	writeList(l)
 	return nil
 }
@@ -16,9 +18,11 @@
 
 func listall(c *command) error {
 	l, err := layoutFromTodoFile()
-	if err != nil && c.args[0] != "create" {
+	if err != nil {
 		return err
 	}
+	//d := dagFromLayout(l)
+	//writeListAll(d)
 	writeListAll(l)
 	return nil
 }
@@ -43,9 +47,9 @@
 	defer writeTodo(l)
 	switch c.args[0] {
 	case "parent":
-		l.rmLink(parent, c.args[1], c.args[2])
+		l.rmLink(c.args[2], c.args[1])
 	case "child":
-		l.rmLink(child, c.args[1], c.args[2])
+		l.rmLink(c.args[1], c.args[1])
 	default:
 		return fmt.Errorf("Command not supported: %v", c.args[0])
 	}
@@ -57,29 +61,29 @@
 		return fmt.Errorf("Incorrect arguments supplied to add")
 	}
 	l, err := layoutFromTodoFile()
-	if err != nil && c.args[0] != "create" {
+	if err != nil {
 		return err
 	}
 	switch c.args[0] {
 	case "parent":
-		l.addLink(parent, c.args[1], c.args[2])
+		l.addLink(c.args[2], c.args[1])
 	case "child":
-		l.addLink(child, c.args[1], c.args[2])
+		l.addLink(c.args[1], c.args[2])
 	default:
-		return fmt.Errorf("Command not supported: %v", c.args[0])
+		return fmt.Errorf("Command not supported: %v %v", c.args[0], c.args[1])
 	}
+	writeTodo(l)
 	return nil
 }
 
 // generate walks the file looking for a handful of known tokens
 func generate(c *command) error {
-	//g := newGenerator()
-	//g.Compare(l)
-	// WalkFunc through and search each file. We do want to early exit on an unsupported mime
-	// We'll use a codified list of mappings for now, PR to update.
-	// use http.DetectContentType(data) and send off to a go routine when it's a known type
-	// Return on channel to run l.command on for the given types, run commands in main thread until done
-	// In the end, we'll have a workable layout
+	g := newGenerator()
+	if g.dotTodoExists() {
+		g.parseTodo()
+	}
+	l := g.toLayout()
+	writeTodo(l)
 	return nil
 }
 
@@ -93,9 +97,6 @@
 	}
 	switch c.args[0] {
 	case "create":
-		l = &Layout{
-			TaskList: []*Entries{},
-		}
 		err = l.create(c.args[1])
 		if err != nil {
 			return err
--- a/samples/.todo
+++ b/samples/.todo
@@ -1,19 +1,19 @@
-#1: 
+[#1]: 
 TODO(halfwit) - Write all the code
 [ ] general: Show what things look like when added manually
 [ ] general: Show what auto generated looks like as well
 
-#2: requires [#1]
+[#2]: requires [#1]
 TODO(halfwit) - remember how to write c code on normal systems
 [ ] sample.c: write the stuff
 [ ] sample.c: don't get hung up on the details
 
-#3: requires [#1]
+[#3]: requires [#1]
 TODO(halfwit) - Make a demo of something interesting to demo `todo generate`
 [ ] sample.go: write the code
 [ ] sample.go: take the time
 
-#4: requires [#1] [example]
+[#4]: requires [#1] [example]
 TODO(halfwit) - Write some sample code
 [ ] sample.sh: get to the choppa
 [ ] sample.sh: return
@@ -20,6 +20,6 @@
 INFO - Show adding multiple components to a single task from other source files
 [x] file2.sh: Add another todo entry for the same tag
 
-example:
+[example]:
 [x] general: Show how headers are optional
 [x] general: Show something silly
\ No newline at end of file
--- a/todo.go
+++ b/todo.go
@@ -16,7 +16,6 @@
 		flag.Usage()
 		os.Exit(0)
 	}
-	// Build and run our command
 	cmd, err := newCommand(flag.Arg(0))
 	if err != nil {
 		log.Fatal(err)
--- a/write.go
+++ b/write.go
@@ -6,10 +6,12 @@
 	"text/template"
 )
 
-const tmplt = `{{range .TaskList}}{{.Title}}
-{{range .List}}{{if .Done}}[x] {{.Desc}}{{else}}[ ] {{.Desc}}{{end}}
+// Templates are so fun.
+const todoFile = `{{range .Jobs}}{{range $n, $t := .Tags}}{{if $n}} {{end}}[{{$t}}]{{end}}:{{ $length := len .Requires}}{{if gt $length 0}} requires{{range .Requires}} [{{.}}]{{end}}{{end}}
+{{range .Tasks}}{{if .Title}}{{.Title}}
+{{end}}{{range .Entries}}{{if .Done}}[x] {{.Desc}}{{else}}[ ] {{.Desc}}{{end}}
 {{end}}
-{{end}}`
+{{end}}{{end}}`
 
 // Writer that creates our normal file
 func writeTodo(l *Layout) {
@@ -18,7 +20,7 @@
 	if err != nil {
 		log.Fatal(err)
 	}
-	t := template.Must(template.New("tmplt").Parse(tmplt))
+	t := template.Must(template.New("todoFile").Parse(todoFile))
 	err = t.Execute(wr, l)
 	if err != nil {
 		log.Fatal(err)