├── LICENSE.md ├── README.md ├── exgraph.png └── taskgv.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 John O. Brickley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # taskgv 2 | Generates a pretty, directed graph of [Taskwarrior](http://taskwarrior.org) projects, tags, and tasks. After the graph has been generated, it's opened using either `xdg-open` (under GNU/Linux) or `open` (under macOS). 3 | 4 | Things end up looking kinda like this: 5 | 6 | ![](exgraph.png) 7 | 8 | Arrows show implication, so they point from projects to tasks, and from tasks to tags. That is, projects _include_ tasks and tasks _include_ tags. 9 | 10 | Projects are represented by large circles, tasks by colored rectangles, and tags by uncolored squares. 11 | 12 | ## Requirements 13 | 14 | * Either of the `xdg-open` or `open` commands, present by default on most GNU/Linux or macOS installs, respectively. 15 | 16 | * Python 17 | 18 | * Taskwarrior 19 | 20 | * [Graphviz](http://www.graphviz.org/)'s `digraph` command. 21 | 22 | ## Usage 23 | 24 | When placed in your $PATH, `taskgv.py ` opens a graph of your business, filtered by Taskwarrior ``. 25 | 26 | ## Integration 27 | 28 | This command can be integrated with Taskwarrior by either running `task config alias.gv execute taskgv.py` or adding the line `alias.gv=execute taskgv.py` to your `.taskrc`. 29 | In either case, the result will be the ability to call taskgv with the command `task gv`. 30 | 31 | ### Credit 32 | 33 | This script is derived from [graphdeps.py](http://taskwarrior.org/projects/taskwarrior/wiki/ExternalScripts#graphdepspy). 34 | -------------------------------------------------------------------------------- /exgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shushcat/taskgv/80c5bb20d9cf223f92dd89c5d517cfdfac7cf8f4/exgraph.png -------------------------------------------------------------------------------- /taskgv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 'graph dependencies in projects' 3 | import json 4 | from subprocess import Popen, PIPE 5 | import subprocess 6 | import sys 7 | import textwrap 8 | from distutils import spawn 9 | 10 | # Typical command line usage: 11 | # 12 | # taskgv TASKFILTER 13 | # 14 | # TASKFILTER is a taskwarrior filter, documentation can be found here: http://taskwarrior.org/projects/taskwarrior/wiki/Feature_filters 15 | # 16 | # Probably the most helpful commands are: 17 | # 18 | # taskgv project:fooproject status:pending 19 | # --> graph pending tasks in project 'fooproject' 20 | # 21 | # taskgv project:fooproject 22 | # --> graphs all tasks in 'fooproject', pending, completed, deleted 23 | # 24 | # taskgv status:pending 25 | # --> graphs all pending tasks in all projects 26 | # 27 | # taskgv 28 | # --> graphs everything - could be massive 29 | 30 | # Wrap label text at this number of characters. 31 | charsPerLine = 20; 32 | 33 | # Full list of colors here: http://www.graphviz.org/doc/info/colors.html 34 | blockedColor = 'gold4' 35 | maxUrgencyColor = 'red2' 36 | unblockedColor = 'green' 37 | doneColor = 'grey' 38 | waitColor = 'white' 39 | deletedColor = 'pink'; 40 | 41 | # The width of the border around the tasks: 42 | penWidth = 1 43 | 44 | # Let arrow direction show implication. 45 | dir = 'back' 46 | 47 | # Have one HEADER (and only one) uncommented at a time, or the last uncommented value will be the only one considered. 48 | 49 | # Left to right layout: 50 | #HEADER = "digraph dependencies { splines=true; overlap=ortho; rankdir=LR; weight=2;" 51 | 52 | # Spread tasks on page: 53 | HEADER = "digraph dependencies { layout=neato; splines=true; overlap=scalexy; rankdir=LR; weight=2;" 54 | 55 | # More information on setting up Graphviz: http://www.graphviz.org/doc/info/attrs.html 56 | 57 | #-----------------------------------------# 58 | # Editing under this might break things # 59 | #-----------------------------------------# 60 | 61 | FOOTER = "}" 62 | 63 | validUuids = list() 64 | 65 | def call_taskwarrior(cmd): 66 | 'call taskwarrior, returning output and error' 67 | tw = Popen(['task'] + cmd.split(), stdout=PIPE, stderr=PIPE) 68 | return tw.communicate() 69 | 70 | def get_json(query): 71 | 'call taskwarrior, returning objects from json' 72 | result, err = call_taskwarrior('end.after:today xor status:pending export %s' % query) 73 | if err.decode("utf-8") != '': 74 | print ('Error calling taskwarrior:') 75 | print (err.decode("utf-8")) 76 | quit() 77 | return json.loads(result.decode("utf-8")) 78 | 79 | def call_dot(instr): 80 | 'call dot, returning stdout and stdout' 81 | dot = Popen('dot -Tsvg'.split(), stdout=PIPE, stderr=PIPE, stdin=PIPE) 82 | return dot.communicate(instr.encode('utf-8')) 83 | 84 | if __name__ == '__main__': 85 | query = sys.argv[1:] 86 | print ('Calling TaskWarrior') 87 | # Print data. 88 | data = get_json(' '.join(query)) 89 | 90 | maxUrgency = -9999; 91 | for datum in data: 92 | if float(datum['urgency']) > maxUrgency: 93 | maxUrgency = float(datum['urgency']) 94 | 95 | # First pass: labels. 96 | lines = [HEADER] 97 | print ('Printing Labels') 98 | for datum in data: 99 | validUuids.append(datum['uuid']) 100 | if datum['description']: 101 | 102 | style = '' 103 | color = '' 104 | style = 'filled' 105 | 106 | if datum['status']=='pending': 107 | prefix = datum['id'] 108 | if not datum.get('depends','') : color = unblockedColor 109 | else : 110 | hasPendingDeps = 0 111 | for depend in datum['depends'].split(','): 112 | for datum2 in data: 113 | if datum2['uuid'] == depend and datum2['status'] == 'pending': 114 | hasPendingDeps = 1 115 | if hasPendingDeps == 1 : color = blockedColor 116 | else : color = unblockedColor 117 | 118 | elif datum['status'] == 'waiting': 119 | prefix = 'WAIT' 120 | color = waitColor 121 | elif datum['status'] == 'completed': 122 | prefix = 'DONE' 123 | color = doneColor 124 | elif datum['status'] == 'deleted': 125 | prefix = 'DELETED' 126 | color = deletedColor 127 | else: 128 | prefix = '' 129 | color = 'white' 130 | 131 | if float(datum['urgency']) == maxUrgency: 132 | color = maxUrgencyColor 133 | 134 | label = ''; 135 | descriptionLines = textwrap.wrap(datum['description'],charsPerLine); 136 | for descLine in descriptionLines: 137 | label += descLine+"\\n"; 138 | 139 | # Documentation http://www.graphviz.org/doc/info/attrs.html 140 | lines.append('"%s"[shape=box][penwidth=%d][label="%s\:%s"][fillcolor=%s][style=%s]' % (datum['uuid'], penWidth, prefix, label, color, style)) 141 | 142 | # Second pass: dependencies. 143 | print ('Resolving Dependencies') 144 | for datum in data: 145 | if datum['description']: 146 | for dep in datum.get('depends', '').split(','): 147 | #print ("\naaa %s" %dep) 148 | if dep!='' and dep in validUuids: 149 | lines.append('"%s" -> "%s"[dir=%s];' % (dep, datum['uuid'], dir)) 150 | continue 151 | 152 | # Third pass: projects. 153 | print ('Making and Linking Project Nodes') 154 | for datum in data: 155 | for proj in datum.get('project', '').split(','): 156 | if proj != '': 157 | lines.append('"%s" -> "%s"[dir=both][arrowtail=odot];' % (proj, datum['uuid'])) 158 | lines.append('"%s"[shape=circle][fontsize=40.0][penwidth=16][color=gray52]' % (proj)) 159 | continue 160 | 161 | # Third pass: tags. 162 | print ('Making and Linking Tag Nodes') 163 | for datum in data: 164 | for tag in datum.get('tags',''): 165 | if tag != '': 166 | lines.append('"%s" -> "%s";' % (datum['uuid'], tag)) 167 | lines.append('"%s"[shape=square][fontsize=24.0][penwidth=8]' % (tag)) 168 | continue 169 | 170 | lines.append(FOOTER) 171 | 172 | print ('Calling dot') 173 | svg, err = call_dot('\n'.join(lines)) 174 | if err.decode("utf-8") != '': 175 | print ('Error calling dot:') 176 | print (err.decode("utf-8")) 177 | quit() 178 | 179 | print ('Writing to /tmp/taskgv.svg') 180 | with open('/tmp/taskgv.svg', 'wb') as f: 181 | f.write(svg) 182 | 183 | # Use `xdg-open` if it's present, `open` otherwise. 184 | display_command = spawn.find_executable("xdg-open") 185 | if display_command == None: 186 | display_command = spawn.find_executable("open") 187 | 188 | subprocess.call(display_command + " /tmp/taskgv.svg", shell = True) 189 | --------------------------------------------------------------------------------