ref: 2bc3c60c50995de3d1f49d85a97e674b572a28dd
parent: 964151eb40c399b1ee18c2d4f37b7f38b69d514e
author: halfwit <halfwit@Halfwits-MBP.hitronhub.home>
date: Tue Jan 28 02:33:35 PST 2020
Ran through a linter
--- a/README.md
+++ b/README.md
@@ -180,5 +180,5 @@
- `todo dot` can be piped to `graphviz` and friends to view your current task hierarchies
- `$ todo dot | dot -Tpng -o my.png`
+ `$ todo dot | dot -Tpng -o my.png`

--- a/command.go
+++ b/command.go
@@ -1,64 +1,242 @@
package main
import (
- "errors"
"flag"
"fmt"
- "log"
)
+// Supported commands
+const (
+ add = "add"
+ child = "child"
+ create = "create"
+ dot = "dot"
+ generate = "generate"
+ list = "list"
+ listall = "listall"
+ parent = "parent"
+ remove = "rm"
+ task = "task"
+)
+
type command struct {
args []string
- runner func(c *command) error
+ runner func() error
}
func newCommand(arg string) (*command, error) {
c := &command{}
-
- if arg == "task" {
+ if arg == task {
c.args = flag.Args()[1:]
- c.runner = task
+ c.runner = c.task
+
return c, nil
}
- if err := c.setTask(arg); err != nil {
+
+ if err := setTask(c, arg); err != nil {
return nil, err
}
+
return c, nil
}
-func (c *command) setTask(arg string) error {
+func setTask(c *command, arg string) error {
switch arg {
- case "list":
- c.runner = list
- case "listall":
- c.runner = listall
- case "dot":
- c.runner = dot
- case "rm":
+ case list:
+ c.runner = c.list
+ case listall:
+ c.runner = c.listall
+ case dot:
+ c.runner = c.dot
+ case remove:
if flag.NArg() < 2 {
- return errors.New("No arguments supplied to rm")
+ return fmt.Errorf(errNoArg, remove)
}
+
c.args = flag.Args()[1:]
- c.runner = rm
- case "add":
+ c.runner = c.rm
+ case add:
if flag.NArg() < 2 {
- return errors.New("No arguments supplied to add")
+ return fmt.Errorf(errNoArg, add)
}
+
c.args = flag.Args()[1:]
- c.runner = add
- case "generate":
- c.runner = generate
+ c.runner = c.add
+ case generate:
+ c.runner = c.generate
default:
- return fmt.Errorf("Unknown command %q", arg)
+ return fmt.Errorf(errBadArg, arg)
}
+
return nil
}
-func (c *command) run() {
+func (c *command) add() error {
+ if len(c.args) != 3 {
+ return fmt.Errorf(errNoArg, add)
+ }
+
+ l, err := layoutFromTodoFile()
+ if err != nil {
+ return err
+ }
+
+ switch c.args[0] {
+ case parent:
+ l.addLink(c.args[2], c.args[1])
+ case child:
+ l.addLink(c.args[1], c.args[2])
+ default:
+ return fmt.Errorf(errBadArg, c.args[0])
+ }
+
+ writeTodo(l)
+
+ return nil
+}
+
+func (c *command) dot() error {
+ l, err := layoutFromTodoFile()
+ if err != nil && c.args[0] != create {
+ return err
+ }
+
+ writeDot(l)
+
+ return nil
+}
+
+// generate walks the file looking for a handful of known tokens
+func (c *command) generate() error {
+ g := &generator{}
+ if g.dotTodoExists() {
+ err := g.parseTodo()
+ if err != nil {
+ return err
+ }
+ }
+
+ l := g.toLayout()
+ writeTodo(l)
+
+ return nil
+}
+
+func (c *command) list() error {
+ l, err := layoutFromTodoFile()
+ if err != nil {
+ return err
+ }
+
+ writeList(l)
+
+ return nil
+}
+
+func (c *command) listall() error {
+ l, err := layoutFromTodoFile()
+ if err != nil {
+ return err
+ }
+
+ writeListAll(l)
+
+ return nil
+}
+
+func (c *command) rm() error {
+ if len(c.args) != 3 {
+ return fmt.Errorf(errNoArg, remove)
+ }
+
+ l, err := layoutFromTodoFile()
+ if err != nil && c.args[0] != create {
+ return err
+ }
+
+ defer writeTodo(l)
+
+ switch c.args[0] {
+ case "parent":
+ l.rmLink(c.args[2], c.args[1])
+ case "child":
+ l.rmLink(c.args[1], c.args[1])
+ default:
+ return fmt.Errorf(errBadArg, c.args[0])
+ }
+
+ return nil
+}
+
+func (c *command) run() error {
if c.runner != nil {
- err := c.runner(c)
+ err := c.runner()
if err != nil {
- log.Fatal(err)
+ return err
}
+ }
+
+ return nil
+}
+
+func (c *command) task() error {
+ if len(c.args) < 2 {
+ return fmt.Errorf(errNoArg, task)
+ }
+
+ l, err := layoutFromTodoFile()
+ if err != nil && c.args[0] != create {
+ return err
+ }
+
+ switch c.args[0] {
+ case create:
+ err = l.create(c.args[1])
+ if err != nil {
+ return err
+ }
+
+ writeTodo(l)
+
+ return nil
+ case "destroy":
+ err := l.destroy(c.args[1])
+ if err != nil {
+ return err
+ }
+
+ writeTodo(l)
+
+ return nil
+ case "add":
+ if l.taskExists(c.args[1], c.args[2]) {
+ return fmt.Errorf(errExists, c.args[1])
+ }
+
+ if e := l.addTask(c.args[1], c.args[2]); e != nil {
+ return e
+ }
+
+ writeTodo(l)
+
+ return nil
+ case "rm":
+ if !l.taskExists(c.args[1], c.args[2]) {
+ return fmt.Errorf(errNoEntry, c.args[2])
+ }
+
+ defer writeTodo(l)
+
+ return l.rmTask(c.args[1], c.args[2])
+ case "toggle":
+ if !l.taskExists(c.args[1], c.args[2]) {
+ return fmt.Errorf(errNoEntry, c.args[1])
+ }
+
+ defer writeTodo(l)
+
+ return l.toggleTask(c.args[1], c.args[2])
+ default:
+ return fmt.Errorf(errBadArg, c.args[0])
}
}
--- a/dag.go
+++ b/dag.go
@@ -1,6 +1,8 @@
package main
import (
+ "log"
+
"github.com/goombaio/dag"
)
@@ -7,10 +9,14 @@
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])
+ if e := dg.AddVertex(dm[job.Key]); e != nil {
+ log.Println(e)
+ }
}
+
for _, job := range l.Jobs {
for _, req := range job.Requires {
for _, other := range l.Jobs {
@@ -17,12 +23,17 @@
if tagsMatch(other.Tags, job.Tags) {
continue
}
+
if !contains(other.Tags, req) {
continue
}
- dg.AddEdge(dm[job.Key], dm[other.Key])
+
+ if e := dg.AddEdge(dm[job.Key], dm[other.Key]); e != nil {
+ log.Println(e)
+ }
}
}
}
+
return dg
}
--- a/generate.go
+++ b/generate.go
@@ -1,7 +1,7 @@
package main
-
import "os"
+
type generator struct {
existing *Layout
}
@@ -10,15 +10,18 @@
if _, err := os.Stat(".todo"); err != nil {
return false
}
+
return true
}
func (g *generator) parseTodo() error {
var err error
+
g.existing, err = layoutFromTodoFile()
if err != nil {
return err
}
+
return nil
}
--- a/layout.go
+++ b/layout.go
@@ -35,15 +35,17 @@
}
func layoutFromTodoFile() (*Layout, error) {
-
l := &Layout{
Jobs: []*Job{},
}
+
fl, err := os.Open(".todo")
if err != nil {
return nil, err
}
+
sc := bufio.NewScanner(fl)
+
for sc.Scan() {
if sc.Text() == "" {
continue
@@ -56,11 +58,12 @@
j.Requires = parseRequires(sc)
j.Tasks = parseTasks(sc)
l.Jobs = append(l.Jobs, j)
-
}
+
if err := sc.Err(); err != nil {
return nil, err
}
+
return l, nil
}
@@ -67,6 +70,7 @@
func parseTags(sc *bufio.Scanner) []string {
txt := sc.Text()
n := strings.Index(txt, ":")
+
return stringToTags(txt[:n])
}
@@ -73,29 +77,35 @@
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 {
+ if len(title) == 0 {
return tasks
}
+
if strings.HasPrefix(title, "[ ]") || strings.HasPrefix(title, "[x]") {
title = ""
} else {
sc.Scan()
}
+
t := &Task{
Title: title,
Entries: findEntries(sc),
}
+
tasks = append(tasks, t)
}
}
@@ -102,11 +112,13 @@
func findEntries(sc *bufio.Scanner) []*Entry {
en := []*Entry{}
+
for {
d := sc.Text()
if len(d) < 4 {
return en
}
+
switch d[:3] {
case "[ ]":
en = append(en, &Entry{
@@ -121,6 +133,7 @@
default:
return en
}
+
sc.Scan()
}
}
@@ -131,6 +144,7 @@
return true
}
}
+
return false
}
@@ -144,6 +158,7 @@
return false
}
}
+
return true
}
@@ -155,6 +170,7 @@
}
}
}
+
return true
}
@@ -161,9 +177,10 @@
// This is adequate for our needs.
func stringToJobs(incoming string) (*Job, error) {
r := regexp.MustCompile(`([^\[\]]*)\s?(\[.*\])+\s?(.*)`)
+
matches := r.FindAllStringSubmatch(incoming, -1)
if matches != nil && len(matches[0]) < 3 {
- return nil, errors.New("Could not parse string")
+ return nil, errors.New("could not parse string")
}
title := matches[0][1]
@@ -170,13 +187,17 @@
if len(matches[0]) == 4 {
title = fmt.Sprintf("%s%s", matches[0][1], matches[0][3])
}
+
t := &Task{
Title: title,
}
- return &Job{
+
+ j := &Job{
Tags: stringToTags(matches[0][2]),
Tasks: []*Task{t},
- }, nil
+ }
+
+ return j, nil
}
func trimBrackets(incoming string) string {
@@ -186,27 +207,35 @@
func stringToTags(incoming string) []string {
var tags []string
- var tmp string
- var startTag bool
+
+ tmp := ""
+ startTag := false
n := 0
+
for _, b := range []byte(incoming) {
if b == '[' {
startTag = true
tmp = ""
+
continue
}
+
if b == ']' {
- n++
- tags = append(tags, tmp)
startTag = false
+
+ tags = append(tags, tmp)
+ n++
}
+
if startTag {
tmp += string(b)
}
}
+
if tags == nil || tags[0] == "" {
return nil
}
+
return tags
}
@@ -215,9 +244,11 @@
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]
}
}
@@ -224,24 +255,29 @@
func (l *Layout) destroy(title string) error {
if _, err := os.Stat(".todo"); err != nil {
- return errors.New("Unable to locate .todo file")
+ return errors.New(errNoTodo)
}
+
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.Jobs)-1 {
copy(l.Jobs[i:], l.Jobs[i+1:])
}
+
l.Jobs = l.Jobs[:len(l.Jobs)-1]
- return nil
+ return nil
}
- return errors.New("No such Entry")
+
+ return fmt.Errorf(errNoTask, tasks.Key)
}
func (l *Layout) create(incoming string) error {
@@ -249,7 +285,9 @@
if err != nil {
return err
}
+
l.Jobs = append(l.Jobs, tasks)
+
return nil
}
@@ -258,10 +296,12 @@
if err != nil {
return false
}
+
for _, e := range l.Jobs {
if !tagsMatch(e.Tags, t.Tags) {
continue
}
+
for _, t := range e.Tasks {
for _, d := range t.Entries {
if d.Desc == item {
@@ -270,6 +310,7 @@
}
}
}
+
return false
}
@@ -279,14 +320,17 @@
if err != nil {
return err
}
+
entry := &Entry{
Desc: item,
Done: false,
}
+
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)
@@ -294,15 +338,21 @@
}
}
}
+
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
}
@@ -311,24 +361,30 @@
if err != nil {
return err
}
+
for _, e := range l.Jobs {
if !tagsMatch(e.Tags, t.Tags) {
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
}
}
}
- return fmt.Errorf("No such task/Entry")
+
+ return fmt.Errorf(errNoTask, item)
}
func (l *Layout) toggleTask(title, item string) error {
@@ -336,34 +392,42 @@
if err != nil {
return err
}
+
for _, e := range l.Jobs {
if !tagsMatch(e.Tags, t.Tags) {
continue
}
+
for _, t := range e.Tasks {
for _, j := range t.Entries {
if j.Desc != item {
continue
}
+
j.Done = !j.Done
+
return nil
}
}
}
- return fmt.Errorf("No such task/Entry")
+
+ return fmt.Errorf(errNoTask, item)
}
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)
}
}
@@ -376,14 +440,15 @@
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
+++ /dev/null
@@ -1,134 +1,0 @@
-package main
-
-import (
- "errors"
- "fmt"
-)
-
-func list(c *command) error {
- l, err := layoutFromTodoFile()
- if err != nil {
- return err
- }
- writeList(l)
- return nil
-}
-
-func listall(c *command) error {
- l, err := layoutFromTodoFile()
- if err != nil {
- return err
- }
- writeListAll(l)
- return nil
-}
-
-func dot(c *command) error {
- l, err := layoutFromTodoFile()
- if err != nil && c.args[0] != "create" {
- return err
- }
- writeDot(l)
- return nil
-}
-
-func rm(c *command) error {
- if len(c.args) != 3 {
- return fmt.Errorf("Incorrect arguments supplied to rm")
- }
- l, err := layoutFromTodoFile()
- if err != nil && c.args[0] != "create" {
- return err
- }
- defer writeTodo(l)
- switch c.args[0] {
- case "parent":
- l.rmLink(c.args[2], c.args[1])
- case "child":
- l.rmLink(c.args[1], c.args[1])
- default:
- return fmt.Errorf("Command not supported: %v", c.args[0])
- }
- return nil
-}
-
-func add(c *command) error {
- if len(c.args) != 3 {
- return fmt.Errorf("Incorrect arguments supplied to add")
- }
- l, err := layoutFromTodoFile()
- if err != nil {
- return err
- }
- switch c.args[0] {
- case "parent":
- l.addLink(c.args[2], c.args[1])
- case "child":
- l.addLink(c.args[1], c.args[2])
- default:
- 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 := &generator{}
- if g.dotTodoExists() {
- err := g.parseTodo()
- if err != nil {
- return err
- }
- }
- l := g.toLayout()
- writeTodo(l)
- return nil
-}
-
-func task(c *command) error {
- if len(c.args) < 2 {
- return errors.New("Too few arguments supplied")
- }
- l, err := layoutFromTodoFile()
- if err != nil && c.args[0] != "create" {
- return err
- }
- switch c.args[0] {
- case "create":
- err = l.create(c.args[1])
- if err != nil {
- return err
- }
- writeTodo(l)
- return nil
- case "destroy":
- err := l.destroy(c.args[1])
- if err != nil {
- return err
- }
- writeTodo(l)
- return nil
- case "add":
- if l.taskExists(c.args[1], c.args[2]) {
- return fmt.Errorf("Entry exists")
- }
- l.addTask(c.args[1], c.args[2])
- writeTodo(l)
- return nil
- case "rm":
- if !l.taskExists(c.args[1], c.args[2]) {
- return fmt.Errorf("No entry found")
- }
- defer writeTodo(l)
- return l.rmTask(c.args[1], c.args[2])
- case "toggle":
- if !l.taskExists(c.args[1], c.args[2]) {
- return fmt.Errorf("No such task/entry")
- }
- defer writeTodo(l)
- return l.toggleTask(c.args[1], c.args[2])
- default:
- return fmt.Errorf("Command not supported: %v", c.args[0])
- }
-}
--- a/todo.go
+++ b/todo.go
@@ -1,24 +1,15 @@
package main
import (
- "flag"
"log"
"os"
)
-var (
- mkfile = flag.String("m", "", "Alternate Makefile to use")
-)
-
func main() {
- flag.Parse()
- if flag.Lookup("h") != nil {
- flag.Usage()
- os.Exit(0)
- }
- cmd, err := newCommand(flag.Arg(0))
+ cmd, err := newCommand(os.Args[0])
if err != nil {
log.Fatal(err)
}
- cmd.run()
+
+ log.Fatal(cmd.run())
}
--- a/write.go
+++ b/write.go
@@ -21,11 +21,14 @@
// Writer that creates our normal file
func writeTodo(l *Layout) {
wr, err := os.Create(".todo")
- defer wr.Close()
if err != nil {
log.Fatal(err)
}
+
+ defer wr.Close()
+
t := template.Must(template.New("todoTmpl").Parse(todoTmpl))
+
err = t.Execute(wr, l)
if err != nil {
log.Fatal(err)
@@ -35,34 +38,42 @@
// 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 {
+
+ switch len(job.Tasks) {
+ case 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 {
+ default:
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")
}
@@ -71,10 +82,12 @@
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())
}
@@ -84,12 +97,16 @@
l.removeCompleted()
d := dagFromLayout(l)
leaves := d.SinkVertices()
+
for _, leaf := range leaves {
job := leaf.Value.(*Job)
+
for _, t := range job.Tasks {
fmt.Printf("%v - %s\n", job.Tags, t.Title)
+
for _, e := range t.Entries {
var f rune
+
switch e.Done {
case true:
f = '✓'
@@ -96,6 +113,7 @@
case false:
f = '✗'
}
+
fmt.Printf(" %c %s\n", f, e.Desc)
}
}
@@ -104,26 +122,34 @@
// Writer that outputs all nodes
func writeListAll(l *Layout) {
+ var walk func(v *dag.Vertex)
+
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] - %s\n", job.Key, t.Title)
+
for _, e := range t.Entries {
var f rune
+
switch e.Done {
case true:
f = '✓'
@@ -130,10 +156,12 @@
case false:
f = '✗'
}
+
fmt.Printf(" %c %s\n", f, e.Desc)
}
}
}
+
for _, t := range d.SourceVertices() {
walk(t)
}