Skip to content

Instantly share code, notes, and snippets.

@punkeel
Last active July 9, 2023 23:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save punkeel/e84cf1e55995d98d6d7a9822e401f74b to your computer and use it in GitHub Desktop.
Save punkeel/e84cf1e55995d98d6d7a9822e401f74b to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# cat /usr/local/sbin/taskgraph.py
# https://gist.github.com/punkeel/e84cf1e55995d98d6d7a9822e401f74b
# Little script to display TaskWarrior tasks in a graph, along with their dependencies
# alias td='rm -f ~/tasks.png; task export status:pending | ~/taskgraph.py > ~/tasks.png && echo Saved tasks graph to ~/tasks.png'
import json
import sys
import textwrap
try:
from graphviz import Digraph
except:
print('ERROR: Could not import graphviz.', file=sys.stderr)
print('Please install it using the following command:', file=sys.stderr)
print('pip3 install graphviz', file=sys.stderr)
sys.exit(1)
# Maps a project name to a subgraph
projects = {}
# List of all the tasks' UUIDs we've encountered
all_nodes = []
# List of task UUIDs with at least one dependency
tasks_with_deps = set()
# Format: png, svg, ...
format = 'png'
# Main graph
dot = Digraph(comment='My Todo List',
node_attr={'shape': 'box',
'style': 'filled',
'color': 'lightblue2'})
data = json.load(sys.stdin)
for task in data:
if task['status'] == 'deleted':
continue
_graph = dot
if 'project' in task:
projectName = task['project']
clusterName = 'cluster_' + projectName
if projectName not in projects:
projects[projectName] = graph = Digraph(name=clusterName,
node_attr={'shape': 'box'})
graph.attr(rankdir='LR')
graph.attr(style='dotted')
graph.attr(label=projectName, fontsize='20')
_graph = projects[projectName]
all_nodes.append(task['uuid'])
# Draw the dependency graph
for task in data:
if 'depends' not in task:
continue
dependencies = task["depends"]
for dependency in dependencies.split(","):
if dependency not in all_nodes:
# This dependency is not shown on the graph, so we don't
# want an edge. This happens when the "dependency" task is
# already marked as done, or was removed.
continue
dot.edge(dependency, task['uuid'], label='unlocks', fontsize='9')
tasks_with_deps.add(task['uuid'])
for task in data:
if task['status'] == 'deleted':
continue
_graph = projects[task['project']] if 'project' in task else dot
color = 'gray96'
if task['uuid'] not in tasks_with_deps:
color = 'lightskyblue1'
if task['urgency'] > 10:
color = 'deepskyblue'
if task['status'] == 'waiting':
color = 'gray70'
description = task['description'] + ' [' + str(task['id']) + ']'
description = '\n'.join(textwrap.wrap(description, width=40))
_graph.node(task['uuid'], description, color=color)
# We draw the subprojects on the main graph
for p in projects:
dot.subgraph(projects[p])
sys.stdout.buffer.write(dot.pipe(format))
sys.stdout.flush()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment