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