func baseDirs(root string, includePatterns []string) ([]string, []string) {
	root = filepath.FromSlash(root)
	bases := make([]string, len(includePatterns))
	newincludes := includePatterns[:]
	for i, v := range includePatterns {
		bdir, trailer := filter.SplitPattern(v)
		if !filepath.IsAbs(bdir) {
			bdir = filepath.Join(root, filepath.FromSlash(bdir))
		}
		if stat, err := os.Lstat(bdir); err == nil {
			if stat.Mode()&os.ModeSymlink != 0 {
				lnk, err := os.Readlink(bdir)
				if err != nil {
					continue
				}
				if filepath.IsAbs(lnk) {
					bdir = lnk
				} else {
					bdir = filepath.Join(bdir, lnk)
				}
				if trailer != "" {
					newincludes[i] = bdir + "/" + trailer
				} else {
					newincludes[i] = bdir
				}
			} else {
				bdir = enclosingDir(bdir)
				if bdir == "" {
					bdir = root
				}
			}
		} else {
			bdir = enclosingDir(bdir)
			if bdir == "" {
				bdir = root
			}
		}
		bases[i] = bdir
	}
	return newincludes, bases
}
Given a set of include patterns relative to a root, which directories do we
need to monitor for changes? Returns a modified set of includes ready to pass
to a post filter, and a set of base directories
package moddwatch
import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"
	"github.com/cortesi/moddwatch/filter"
	"github.com/rjeczalik/notify"
)
const MaxLullWait = time.Second * 8
func isUnder(parent string, child string) bool {
	parent = filepath.ToSlash(parent)
	child = filepath.ToSlash(child)
	off := strings.Index(child, parent)
	if off == 0 && (len(child) == len(parent) || child[len(parent)] == '/') {
		return true
	}
	return false
}
func normPaths(root string, abspaths []string) ([]string, error) {
	aroot, err := filepath.Abs(root)
	if err != nil {
		return nil, err
	}
	ret := make([]string, len(abspaths))
	for i, p := range abspaths {
		norm, err := filepath.Abs(p)
		if err != nil {
			return nil, err
		}
		if isUnder(aroot, norm) {
			norm, err = filepath.Rel(aroot, norm)
			if err != nil {
				return nil, err
			}
		}
		ret[i] = filepath.ToSlash(norm)
	}
	return ret, nil
}
type existenceChecker interface {
	Check(p string) bool
}
type statExistenceChecker struct{}
func (sc statExistenceChecker) Check(p string) bool {
	fi, err := os.Stat(p)
	if err == nil && !fi.IsDir() {
		return true
	}
	return false
}
type Mod struct {
	Changed []string
	Deleted []string
	Added   []string
}
func (mod Mod) String() string {
	return fmt.Sprintf(
		"Added: %v\nDeleted: %v\nChanged: %v",
		mod.Added, mod.Deleted, mod.Changed,
	)
}
func (mod Mod) All() []string {
	all := make(map[string]bool)
	for _, p := range mod.Changed {
		all[p] = true
	}
	for _, p := range mod.Added {
		all[p] = true
	}
	return _keys(all)
}
func (mod Mod) Has(p string) bool {
	for _, v := range mod.All() {
		if filepath.Clean(p) == filepath.Clean(v) {
			return true
		}
	}
	return false
}
func (mod Mod) Empty() bool {
	if (len(mod.Changed) + len(mod.Deleted) + len(mod.Added)) > 0 {
		return false
	}
	return true
}
func joinLists(a []string, b []string) []string {
	m := map[string]bool{}
	for _, v := range a {
		m[v] = true
	}
	for _, v := range b {
		m[v] = true
	}
	ret := make([]string, len(m))
	i := 0
	for k := range m {
		ret[i] = k
		i++
	}
	sort.Strings(ret)
	return ret
}
func (mod Mod) Join(b Mod) Mod {
	return Mod{
		Changed: joinLists(mod.Changed, b.Changed),
		Deleted: joinLists(mod.Deleted, b.Deleted),
		Added:   joinLists(mod.Added, b.Added),
	}
}
func (mod Mod) Filter(root string, includes []string, excludes []string) (*Mod, error) {
	changed, err := filter.Files(mod.Changed, includes, excludes)
	if err != nil {
		return nil, err
	}
	deleted, err := filter.Files(mod.Deleted, includes, excludes)
	if err != nil {
		return nil, err
	}
	added, err := filter.Files(mod.Added, includes, excludes)
	if err != nil {
		return nil, err
	}
	return &Mod{Changed: changed, Deleted: deleted, Added: added}, nil
}
func (mod *Mod) normPaths(root string) (*Mod, error) {
	changed, err := normPaths(root, mod.Changed)
	if err != nil {
		return nil, err
	}
	deleted, err := normPaths(root, mod.Deleted)
	if err != nil {
		return nil, err
	}
	added, err := normPaths(root, mod.Added)
	if err != nil {
		return nil, err
	}
	return &Mod{Changed: changed, Deleted: deleted, Added: added}, nil
}
func _keys(m map[string]bool) []string {
	if len(m) > 0 {
		keys := make([]string, len(m))
		i := 0
		for k := range m {
			keys[i] = k
			i++
		}
		sort.Strings(keys)
		return keys
	}
	return nil
}
type fset map[string]bool
func mkmod(exists existenceChecker, added fset, removed fset, changed fset, renamed fset) Mod {
	ret := Mod{}
	for k := range renamed {
		if exists.Check(k) {
			added[k] = true
		} else {
			removed[k] = true
		}
	}
	for k := range added {
		if exists.Check(k) {
			delete(changed, k)
			delete(removed, k)
		} else {
			delete(added, k)
			delete(removed, k)
			delete(changed, k)
		}
	}
	for k := range removed {
		if exists.Check(k) {
			delete(removed, k)
		} else {
			delete(added, k)
			delete(changed, k)
		}
	}
	ret.Added = _keys(added)
	ret.Changed = _keys(changed)
	ret.Deleted = _keys(removed)
	return ret
}
func batch(lullTime time.Duration, maxTime time.Duration, exists existenceChecker, ch chan notify.EventInfo) *Mod {
	added := make(map[string]bool)
	removed := make(map[string]bool)
	changed := make(map[string]bool)
	renamed := make(map[string]bool)
	hadLullMod := false
	for {
		select {
		case evt := <-ch:
			if evt == nil {
				return nil
			}
			hadLullMod = true
			switch evt.Event() {
			case notify.Create:
				added[evt.Path()] = true
			case notify.Remove:
				removed[evt.Path()] = true
			case notify.Write:
				changed[evt.Path()] = true
			case notify.Rename:
				renamed[evt.Path()] = true
			}
		case <-time.After(lullTime):
			if hadLullMod == false {
				m := mkmod(exists, added, removed, changed, renamed)
				return &m
			}
			hadLullMod = false
		case <-time.After(maxTime):
			m := mkmod(exists, added, removed, changed, renamed)
			return &m
		}
	}
}
type Watcher struct {
	evtch  chan notify.EventInfo
	modch  chan *Mod
	closed bool
	sync.Mutex
}
func (w *Watcher) send(m *Mod) {
	w.Lock()
	defer w.Unlock()
	if !w.closed {
		w.modch <- m
	}
}
func (w *Watcher) Stop() {
	w.Lock()
	defer w.Unlock()
	if !w.closed {
		notify.Stop(w.evtch)
		close(w.modch)
		w.closed = true
	}
}
func enclosingDir(path string) string {
	for {
		if stat, err := os.Lstat(path); err == nil {
			if stat.IsDir() {
				return path
			}
		}
		if path == "" {
			return ""
		}
		path = filepath.Dir(path)
	}
}