summary refs log tree commit diff
path: root/.config/waybar/modules/todo.go
diff options
context:
space:
mode:
Diffstat (limited to '.config/waybar/modules/todo.go')
-rw-r--r--.config/waybar/modules/todo.go246
1 files changed, 246 insertions, 0 deletions
diff --git a/.config/waybar/modules/todo.go b/.config/waybar/modules/todo.go
new file mode 100644
index 0000000..449f991
--- /dev/null
+++ b/.config/waybar/modules/todo.go
@@ -0,0 +1,246 @@
+// # Documentation
+// we try to read a file pointed to by WAYBAR_TODO_FILE
+// or `nb show --path todo`.
+// While no particular format is assumed, we recommend
+// Markdown.
+// We extract the todos from the top-level list items
+// (listings start with a `*` character).
+//
+// # Usage:
+// go build -o waybar-todo todo.go
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"os/signal"
+	"regexp"
+	"strings"
+	"text/scanner"
+	"time"
+
+	"github.com/fsnotify/fsnotify"
+	"golang.org/x/sys/unix"
+)
+
+const (
+	TODO_LINE_CHAR       rune   = '*'
+	TODO_INACTIVE_STRING string = ""
+	TODO_LABEL_REGEX     string = `\[\[!(.*)\]\]`
+)
+
+var (
+	active      = true
+	todofile    = os.Getenv("WAYBAR_TODO_FILE")
+	cookiefile  = os.ExpandEnv("${XDG_RUNTIME_DIR}/waybar-todo.cookie")
+	labelFilter = os.Getenv("WAYBAR_TODO_FILTER")
+)
+
+func printJson(text, tooltip string, class []string, alt string, percentage int) error {
+	return json.NewEncoder(os.Stdout).Encode(map[string]interface{}{
+		"text":       text,
+		"tooltip":    tooltip,
+		"class":      class,
+		"alt":        alt,
+		"percentage": percentage,
+	})
+}
+
+type todoItem struct {
+	text  string
+	label string
+}
+
+func readTodofile(path string) ([]todoItem, error) {
+	content, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("reading file: %v", err)
+	}
+
+	var (
+		sc        scanner.Scanner
+		validLine bool
+		todos     []todoItem
+	)
+	sc.Init(bytes.NewReader(content))
+	sc.Filename = path
+	sc.Whitespace ^= 1<<'\n' | 1<<' ' // don't skip newlines or spaces
+	sc.Mode ^= scanner.GoTokens
+	for tok := sc.Scan(); tok != scanner.EOF; tok = sc.Scan() {
+		switch {
+		// a valid line stats with TODO_LINE_CHAR
+		// followed by a space
+		// we strip them before appending the line array
+		case tok == TODO_LINE_CHAR && sc.Peek() == ' ':
+			validLine = true
+			_ = sc.Scan()                     // swallow the space
+			todos = append(todos, todoItem{}) // grow the array
+
+		case tok == '\n':
+			validLine = false
+			if len(todos) > 0 {
+				idx := len(todos) - 1
+				matches := labelre.FindAllStringSubmatch(todos[idx].text, -1)
+				if matches != nil && len(matches) > 0 {
+					todos[idx].label = matches[0][1]
+					todos[idx].text = strings.TrimSpace(
+						strings.Replace(todos[idx].text, matches[0][0], "", 1))
+				}
+			}
+
+		case validLine:
+			todos[len(todos)-1].text = todos[len(todos)-1].text + sc.TokenText()
+		}
+
+	}
+	return todos, nil
+}
+
+func setCookie(active bool) error {
+	if !active {
+		_, err := os.Create(cookiefile)
+		return err
+	}
+	return os.Remove(cookiefile)
+}
+
+func update() error {
+	if _, err := os.Stat(cookiefile); err == nil {
+		log.Printf("cookie set, we're inactive")
+		active = false
+	}
+	if !active {
+		return printJson(TODO_INACTIVE_STRING, "* inactive *", []string{"off"}, "todo-off", 0)
+	}
+	todos, err := readTodofile(todofile)
+	if err != nil {
+		return err
+	}
+	var (
+		text       string
+		tooltip    string
+		extraClass string
+	)
+	for _, item := range todos {
+		if labelFilter != "" && strings.Contains(labelFilter, item.label) {
+			continue
+		}
+		if text == "" {
+			text = item.text
+			extraClass = item.label
+			continue
+		}
+		// render out the rest of the todos into the tooltip
+		tooltip = strings.Join([]string{tooltip, item.text}, "\n")
+	}
+	return printJson(text, tooltip, []string{"on", extraClass}, "todo-on", 1)
+}
+
+var (
+	labelre *regexp.Regexp
+)
+
+func init() {
+	labelre = regexp.MustCompile(TODO_LABEL_REGEX)
+}
+
+func main() {
+	var (
+		updateC = make(chan os.Signal, 1)
+		cancelC = make(chan os.Signal, 1)
+	)
+	signal.Notify(updateC, unix.SIGUSR1, unix.SIGUSR2, unix.SIGHUP)
+	signal.Notify(cancelC, unix.SIGTERM, unix.SIGINT)
+
+	var err error
+	// we will receive a SIGTERM if our parent dies
+	err = unix.Prctl(unix.PR_SET_PDEATHSIG, uintptr(unix.SIGTERM), 0, 0, 0)
+	if err != nil {
+		log.Fatalf("error: setting prctl: %v", err)
+	}
+
+	if todofile == "" {
+		// get from nb(1)
+		nb := exec.Command("sh", "-c", "nb show --no-color --path todo")
+		nb_output, err := nb.Output()
+		if err != nil {
+			e := err.(*exec.ExitError)
+			log.Fatalf("error: finding todofile via nb: %s", e.Stderr)
+		}
+		todofile = strings.TrimSpace(string(nb_output))
+	}
+
+	log.Printf("watching %q", todofile)
+
+	w, err := fsnotify.NewWatcher()
+	if err != nil {
+		log.Fatalf("error: creating watcher: %v", err)
+	}
+	defer w.Close()
+
+	if err := w.Add(todofile); err != nil {
+		log.Fatalf("error: adding watch: %v", err)
+	}
+
+	if err := update(); err != nil {
+		// fail here because it's the first time we
+		// try to read the todofile.
+		log.Fatalf("error: %v", err)
+	}
+	for {
+		select {
+		case evt, ok := <-w.Events:
+			if !ok {
+				return
+			}
+			// see https://github.com/fsnotify/fsnotify/issues/94
+			//     https://github.com/gohugoio/hugo/pull/4720
+			if evt.Op&fsnotify.Remove == fsnotify.Remove {
+				counter := 0
+				for w.Add(todofile) != nil {
+					counter++
+					if counter >= 1000 {
+						log.Panic("error: unable to add file watcher")
+					}
+					time.Sleep(100 * time.Millisecond)
+				}
+			}
+			goto force_update
+		case err, ok := <-w.Errors:
+			if !ok {
+				return
+			}
+			log.Printf("error: from watcher: %v", err)
+
+		case sig := <-updateC:
+			switch sig {
+			case unix.SIGUSR1: // on
+				active = !active
+				if err := setCookie(active); err != nil {
+					log.Printf("err: %v", err)
+				}
+				goto force_update
+			case unix.SIGUSR2: //
+				if strings.Contains(labelFilter, "work") {
+					labelFilter = strings.Trim(strings.ReplaceAll(labelFilter, "work", ""), ",")
+				} else {
+					labelFilter = strings.Join([]string{labelFilter, "work"}, ",")
+				}
+				continue
+			case unix.SIGHUP: // force update
+				log.Println("force update")
+				goto force_update
+			}
+		case <-cancelC:
+			log.Printf("%s exiting", os.Args[0])
+			os.Exit(0)
+		}
+	force_update:
+		_ = update()
+	}
+}