hlfw.ca

todo2

Download patch

ref: 460d2d0fcf6cb75fabd5001bec5d0eb96dcc4458
parent: 4b410a5ffd714421418b90186dede34fae8e8f7e
author: Michael Misch <michaelmisch1985@gmail.com>
date: Fri Nov 29 12:07:49 PST 2019

Update parser for new format and write list and dot outputs

--- /dev/null
+++ b/dag.go
@@ -1,0 +1,28 @@
+package main
+
+import (
+	"github.com/goombaio/dag"
+)
+
+func dagFromLayout(l *Layout) *dag.DAG {
+	dg := dag.NewDAG()
+	dm := make(map[string]*dag.Vertex)
+	for _, job := range l.Jobs {
+		dm[job.Key] = dag.NewVertex(job.Key, job)
+		dg.AddVertex(dm[job.Key])
+	}
+	for _, job := range l.Jobs {
+		for _, req := range job.Requires {
+			for _, other := range l.Jobs {
+				if tagsMatch(other.Tags, job.Tags) {
+					continue
+				}
+				if !contains(other.Tags, req) {
+					continue
+				}
+				dg.AddEdge(dm[job.Key], dm[other.Key])
+			}
+		}
+	}
+	return dg
+}
--- a/generate.go
+++ b/generate.go
@@ -1,27 +1,35 @@
 package main
 
-// 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
 
+import "os"
 type generator struct {
+	existing *Layout
 }
 
-func newGenerator() *generator {
-	return nil
-}
-
 func (g *generator) dotTodoExists() bool {
-	return false
+	if _, err := os.Stat(".todo"); err != nil {
+		return false
+	}
+	return true
 }
 
-func (g *generator) parseTodo() {
-
+func (g *generator) parseTodo() error {
+	var err error
+	g.existing, err = layoutFromTodoFile()
+	if err != nil {
+		return err
+	}
+	return nil
 }
 
+// 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
 // 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 {
+	// Use g.existing as a starting point and walk through our file path, add anything we can find.
+	//go g.parseFile(filename)
 	return nil
 }
--- a/layout.go
+++ b/layout.go
@@ -4,7 +4,6 @@
 	"bufio"
 	"errors"
 	"fmt"
-	"log"
 	"os"
 	"regexp"
 	"strings"
@@ -17,6 +16,7 @@
 
 // Job - A tagged collection of tasks
 type Job struct {
+	Key      string
 	Tags     []string
 	Requires []string
 	Tasks    []*Task
@@ -45,28 +45,18 @@
 	}
 	sc := bufio.NewScanner(fl)
 	for sc.Scan() {
-		txt := sc.Text()
-		if txt == "" {
+		if sc.Text() == "" {
 			continue
 		}
 		// 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
-		}
-		if txt[:1] != "[" {
-			t.Title = txt
-			sc.Scan()
-		}
-		t.Entries = findEntries(sc)
-		j.Tasks = append(j.Tasks, t)
+		j.Tags = parseTags(sc)
+		j.Key = strings.Join(j.Tags, ", ")
+		j.Requires = parseRequires(sc)
+		j.Tasks = parseTasks(sc)
 		l.Jobs = append(l.Jobs, j)
+
 	}
 	if err := sc.Err(); err != nil {
 		return nil, err
@@ -74,18 +64,38 @@
 	return l, nil
 }
 
-func parseTags(txt string) []string {
+func parseTags(sc *bufio.Scanner) []string {
+	txt := sc.Text()
 	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 "):])
+func parseRequires(sc *bufio.Scanner) []string {
+	txt := sc.Text()
+	defer sc.Scan()
+	n := strings.Index(txt, ": requires ")
+	if len(txt) > n+len(": requires [ ]") {
+		return stringToTags(txt[n+len(": requires "):])
 	}
 	return nil
 }
+
+func parseTasks(sc *bufio.Scanner) []*Task {
+	var tasks []*Task
+	for {
+		title := sc.Text()
+		if len(sc.Text()) == 0 {
+			return tasks
+		}
+		sc.Scan()
+		t := &Task{
+			Title:   title,
+			Entries: findEntries(sc),
+		}
+		tasks = append(tasks, t)
+	}
+}
+
 func findEntries(sc *bufio.Scanner) []*Entry {
 	en := []*Entry{}
 	for {
@@ -93,9 +103,6 @@
 		if len(d) < 4 {
 			return en
 		}
-		if err := sc.Err(); err != nil {
-			log.Fatal(err)
-		}
 		switch d[:3] {
 		case "[ ]":
 			en = append(en, &Entry{
@@ -136,13 +143,25 @@
 	return true
 }
 
+func isCompleted(job *Job) bool {
+	for _, task := range job.Tasks {
+		for _, entry := range task.Entries {
+			if !entry.Done {
+				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 {
+	if matches != nil && 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])
@@ -181,10 +200,22 @@
 			tmp += string(b)
 		}
 	}
-	if tags[0] == "" {
+	if tags == nil || tags[0] == "" {
 		return nil
 	}
 	return tags
+}
+
+func (l *Layout) removeCompleted() {
+	for i, job := range l.Jobs {
+		if !isCompleted(job) {
+			continue
+		}
+		if i < len(l.Jobs)-1 {
+			copy(l.Jobs[i:], l.Jobs[i+1:])
+		}
+		l.Jobs = l.Jobs[:len(l.Jobs)-1]
+	}
 }
 
 func (l *Layout) destroy(title string) error {
--- a/runners.go
+++ b/runners.go
@@ -10,8 +10,6 @@
 	if err != nil {
 		return err
 	}
-	//d := dagFromLayout(l)
-	//writeList(d)
 	writeList(l)
 	return nil
 }
@@ -21,8 +19,6 @@
 	if err != nil {
 		return err
 	}
-	//d := dagFromLayout(l)
-	//writeListAll(d)
 	writeListAll(l)
 	return nil
 }
@@ -78,9 +74,12 @@
 
 // generate walks the file looking for a handful of known tokens
 func generate(c *command) error {
-	g := newGenerator()
+	g := &generator{}
 	if g.dotTodoExists() {
-		g.parseTodo()
+		err := g.parseTodo()
+		if err != nil {
+			return err
+		}
 	}
 	l := g.toLayout()
 	writeTodo(l)
--- a/samples/.todo
+++ b/samples/.todo
@@ -1,25 +1,25 @@
-[#1]: 
-TODO(halfwit) - Write all the code
+[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]
-TODO(halfwit) - remember how to write c code on normal systems
+[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]
-TODO(halfwit) - Make a demo of something interesting to demo `todo generate`
+[3]: requires [1] [2]
+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]
-TODO(halfwit) - Write some sample code
-[ ] sample.sh: get to the choppa
+[4]: requires [1] [example] [2] [3]
+TODO(halfwit): Write some sample code
+[x] sample.sh: get to the choppa
 [ ] sample.sh: return
-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]:
+I guess they shouldn't be optional
 [x] general: Show how headers are optional
-[x] general: Show something silly
\ No newline at end of file
+[x] general: Show something silly
+
--- a/write.go
+++ b/write.go
@@ -1,16 +1,21 @@
 package main
 
 import (
+	"fmt"
 	"log"
 	"os"
+	"strings"
 	"text/template"
+
+	"github.com/goombaio/dag"
 )
 
 // 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}}
+const todoTmpl = `{{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}}{{range .Entries}}{{if .Done}}[x] {{.Desc}}
+{{else}}[ ] {{.Desc}}
+{{end}}{{end}}
 {{end}}{{end}}`
 
 // Writer that creates our normal file
@@ -20,7 +25,7 @@
 	if err != nil {
 		log.Fatal(err)
 	}
-	t := template.Must(template.New("todoFile").Parse(todoFile))
+	t := template.Must(template.New("todoTmpl").Parse(todoTmpl))
 	err = t.Execute(wr, l)
 	if err != nil {
 		log.Fatal(err)
@@ -29,15 +34,87 @@
 
 // Writer that outputs in dot format
 func writeDot(l *Layout) {
+	var sb strings.Builder
+	sb.WriteString("digraph depgraph {\n\trankdir=RL;\n")
+	// Labels
+	for _, job := range l.Jobs {
+		sb.WriteString(fmt.Sprintf("%v", job.Key))
+		sb.WriteString(` [label="`)
+		if len(job.Tasks) > 1 {
+			for _, task := range job.Tasks {
+				sb.WriteString(task.Title)
+				for _, entry := range task.Entries {
+					sb.WriteString("\n")
+					sb.WriteString(entry.Desc)
+					if entry.Done {
+						sb.WriteString(" ✓")
+					}
 
+				}
+				sb.WriteString("\n")
+			}
+		} else {
+			sb.WriteString(job.Tasks[0].Title)
+			for _, entry := range job.Tasks[0].Entries {
+				sb.WriteString("\n")
+				sb.WriteString(entry.Desc)
+				if entry.Done {
+					sb.WriteString(" ✓")
+				}
+			}
+		}
+		sb.WriteString(`"];`)
+		sb.WriteString("\r\n")
+	}
+	// Links
+	for _, job := range l.Jobs {
+		if len(job.Requires) == 0 {
+			continue
+		}
+		for _, req := range job.Requires {
+			sb.WriteString(fmt.Sprintf("%s -> %s;\n", job.Key, req))
+		}
+	}
+	sb.WriteString("}\n")
+	fmt.Println(sb.String())
 }
 
 // Writer that outputs only leaves
 func writeList(l *Layout) {
-
+	l.removeCompleted()
+	d := dagFromLayout(l)
+	leaves := d.SinkVertices()
+	for _, leaf := range leaves {
+		job := leaf.Value.(*Job)
+		for _, t := range job.Tasks {
+			fmt.Printf("%v\t%s\n", job.Tags, t.Title)
+		}
+	}
 }
 
 // Writer that outputs all nodes
 func writeListAll(l *Layout) {
-
+	d := dagFromLayout(l)
+	seen := map[*Job]bool{}
+	var walk func(*dag.Vertex)
+	walk = func(v *dag.Vertex) {
+		job := v.Value.(*Job)
+		if seen[job] {
+			return
+		}
+		seen[job] = true
+		children, err := d.Successors(v)
+		if err != nil {
+			log.Fatal(err)
+		}
+		for _, child := range children {
+			walk(child)
+		}
+		for _, t := range job.Tasks {
+			fmt.Printf("[%v]\t%s\n", job.Key, t.Title)
+		}
+	}
+	for _, t := range d.SourceVertices() {
+		walk(t)
+	}
 }