Micah Jamison's Dev Blog.
# Getting Things Done
One of my requirements for a wiki-style markdown note taking app is the ability to keep a running tasks list. This can be as simple as just grabbing a list of tasks with a text search for `- [ ] task` in my markdown files. This worked fine but soon found that I had to hunt for a workable task is a long list of tasks that had dependencies. Trying my best to strike a balance between keeping things simple and text based but not having to reload a dependency graph into my brain every time I wanted to find the next thing to work on, I created a simple script that outputs to a quickfix window in vim.
I structured my tasks into groups. I consider a task group to be any list of tasks in a hierarchy, like this:
```plaintext
# Garden.md
- [ ] Plant Garden
- [ ] create rows
- [ ] add manure
- [ ] add lime
- [ ] till soil
- [ ] Plant seeds [ShoppingList](Home/Shopping.md)
# Shopping.md
...
- [ ] Garden Hose
- [ ] Seeds
```
I like having task groups that are a mixture of composition and dependencies. For example Plant Garden is composed of some tasks that have some dependencies but the grouping is small enough that I don't need to get too concerned with defining them. It's enought to know `create rows` is composed of some subtasks and it can't be started and/or completed until those are done.
Additionally I have a separate file that is a shopping list. I want to define a dependency here of going shopping before I can plant. So we'll just utilize a wiki link so that `Plant seeds` won't be listed as a workable task until shopping is done (aka all tasks in the linked file are marked as done).
I created a script that will generate a quickfix list to show in vim:
```python
#!/usr/bin/python3
import os
import pathlib
import re
from graphlib import TopologicalSorter
GROUPS = r"(?:^[ \t]*[\*\-]? ?\[[ xX]\].*$[\n]?)+"
TASKS_IN_GROUP = r"\s*[\*\-]? ?\[[ xX]\].*$"
WIKI_LINKS = r"\[[^\]]+\]\(([^\)]+)\)"
COMPLETED_TASK = r"\s*[\*\-]? ?\[[xX]\].*$"
class Node:
def __init__(self, name, path, line):
self.name = name
self.path = path
self.line = line
class WikiParser(object):
def __init__(self, path):
self.ts = TopologicalSorter()
self.parse_file(path)
def parse_file(self, path:str, parent=None):
if path.startswith("http"):
return
try:
with open(path, "r") as infile:
data = infile.read()
self.parse_tasks(data, parent, path)
except:
pass
def parse_tasks(self, raw_str, parent=None, path=""):
for i, x in enumerate(re.finditer(GROUPS, raw_str, re.MULTILINE)):
# the breadcrumb path of the current parent
hierarchy = []
start, end = x.span()
# count <CR> before position on match
lineno = raw_str.count('\n', 0, start) + 1
x = x.group()
if parent:
hierarchy.append((-1, parent))
tasks = x.strip("\n").split("\n")
for task in tasks:
# task = f'{prefix}{task}'
indent = task.index("[")
while hierarchy and indent <= hierarchy[-1][0]:
del hierarchy[-1]
if re.match(COMPLETED_TASK, task):
continue
node_task = Node(task, path, lineno)
self.ts.add(node_task)
if hierarchy:
self.ts.add(hierarchy[-1][1], node_task)
hierarchy.append((indent, node_task))
for link in re.findall(WIKI_LINKS, task):
if not link[-3:] == ".md":
link += ".md"
self.parse_file(link, node_task)
def ready(self):
self.ts.prepare()
return tuple(self.ts.get_ready())
def static_order(self):
print(tuple(self.ts.static_order()))
if __name__ == "__main__":
import sys
wp = WikiParser(sys.argv[1])
print("\n".join([f'{t.path}:{t.line}:{t.name.strip()}' for t in wp.ready()]))
sys.exit(1)
```
The script is only using built-in libraries so no need for a virtualenv. Given a current file path, it will return a list of workable tasks in a vim quickfix format.
```plaintext
Home/VimNotes/GettingThingsDone.md:10:- [ ] add manure
Home/VimNotes/GettingThingsDone.md:10:- [ ] add lime
Home/VimNotes/GettingThingsDone.md:10:- [ ] till soil
Home/Shopping.md:3:- [ ] Garden Hose
Home/Shopping.md:3:- [ ] Seeds
```
I then hook this up to vim as a simple system call that pipes to a new quickfix window. This allows using quick navigation and only shows tasks without unfinished dependencies.
```vimscript
nnoremap <leader>g :cexpr system('./quickfix.py ' . expand('%'))<bar>cw<CR>
```
@blog