hlfw.ca

todo2

ref: 2bc3c60c50995de3d1f49d85a97e674b572a28dd
dir: /layout.go/

View raw version
package main

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"regexp"
	"strings"
)

// Layout represents a fully parsed .todo file
type Layout struct {
	Jobs []*Job
}

// Job - A tagged collection of tasks
type Job struct {
	Key      string
	Tags     []string
	Requires []string
	Tasks    []*Task
}

// Task - A collection of Entries (checkboxes)
type Task struct {
	Title   string
	Entries []*Entry
}

// Entry -  single checkbox todo item
type Entry struct {
	Desc string
	Done bool
}

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
		}
		// 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(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
	}

	return l, nil
}

func parseTags(sc *bufio.Scanner) []string {
	txt := sc.Text()
	n := strings.Index(txt, ":")
	
	return stringToTags(txt[:n])
}

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(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)
	}
}

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{
				Desc: d[4:],
				Done: false,
			})
		case "[x]":
			en = append(en, &Entry{
				Desc: d[4:],
				Done: true,
			})
		default:
			return en
		}

		sc.Scan()
	}
}

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
}

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 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])
	}

	t := &Task{
		Title: title,
	}

	j := &Job{
		Tags:  stringToTags(matches[0][2]),
		Tasks: []*Task{t},
	}

	return j, nil
}

func trimBrackets(incoming string) string {
	incoming = strings.TrimPrefix(incoming, "[")
	return strings.TrimSuffix(incoming, "]")
}

func stringToTags(incoming string) []string {
	var tags []string

	tmp := ""
	startTag := false
	n := 0

	for _, b := range []byte(incoming) {
		if b == '[' {
			startTag = true
			tmp = ""

			continue
		}

		if b == ']' {
			startTag = false

			tags = append(tags, tmp)
			n++
		}

		if startTag {
			tmp += string(b)
		}
	}

	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 {
	if _, err := os.Stat(".todo"); err != nil {
		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 fmt.Errorf(errNoTask, tasks.Key)
}

func (l *Layout) create(incoming string) error {
	tasks, err := stringToJobs(incoming)
	if err != nil {
		return err
	}

	l.Jobs = append(l.Jobs, tasks)

	return nil
}

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.Tasks {
			for _, d := range t.Entries {
				if d.Desc == item {
					return true
				}
			}
		}
	}

	return false
}

// 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
	}

	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)
				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 {
	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.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(errNoTask, item)
}

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.Tasks {
			for _, j := range t.Entries {
				if j.Desc != item {
					continue
				}

				j.Done = !j.Done

				return nil
			}
		}
	}

	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)
		}
	}
}

// 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]
			}
		}
	}
}