├── .gitignore ├── .task2dotrc.json ├── LICENSE ├── README.md ├── example.png ├── setup.py ├── task2dot ├── __init__.py └── task2dot.py └── upload2pypi.sh /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.swp 3 | build 4 | dist 5 | out.svg 6 | *egg-info 7 | -------------------------------------------------------------------------------- /.task2dotrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "tags": { 4 | "shape": "egg", 5 | "fillcolor": "#000000", 6 | "penwidth": "2pt", 7 | "style": "filled", 8 | "fontcolor": "white", 9 | "color": "white" 10 | }, 11 | "outcome": { 12 | "shape": "egg", 13 | "fillcolor": "#000000", 14 | "penwidth": "2pt", 15 | "style": "filled", 16 | "fontcolor": "white", 17 | "color": "white" 18 | }, 19 | "people": { 20 | "shape": "octagon", 21 | "style": "filled", 22 | "fillcolor": "#dd9900", 23 | "color": "white" 24 | }, 25 | "project": { 26 | "shape": "diamond", 27 | "penwidth": "2pt", 28 | "color": "#22ff22", 29 | "fontcolor": "#ffffff", 30 | "style": "filled", 31 | "fillcolor": "#115500" 32 | }, 33 | "task": { 34 | "shape": "box", 35 | "color": "white", 36 | "fontcolor": "white", 37 | "style": "rounded,filled", 38 | "fillcolor": "#222299", 39 | "fontsize": "16" 40 | }, 41 | "default": { 42 | "shape": "box", 43 | "color": "white", 44 | "fontcolor": "white", 45 | "style": "rounded,filled", 46 | "fillcolor": "#222299", 47 | "fontsize": "16" 48 | }, 49 | "annotation": { 50 | "shape": "note", 51 | "color": "white", 52 | "fontcolor": "white", 53 | "style": "filled", 54 | "fillcolor": "#111155" 55 | }, 56 | "state": { 57 | "shape": "circle", 58 | "color": "white", 59 | "fontcolor": "white" 60 | } 61 | }, 62 | "edges": { 63 | "default": { 64 | "color": "white" 65 | }, 66 | "task-tags": { 67 | "color": "white", 68 | "style": "dotted", 69 | "penwidth": "5pt", 70 | "arrowsize": "0.1" 71 | }, 72 | "project-project": { 73 | "color": "#22ff22", 74 | "penwidth": "2pt", 75 | "arrowhead": "odot", 76 | "arrowtail": "odot" 77 | }, 78 | "task-project": { 79 | "color": "#aa2211" 80 | } 81 | }, 82 | "graph": { 83 | "layout": "fdp", 84 | "splines": "ortho", 85 | "size": "30,30", 86 | "bgcolor": "#111519" 87 | } 88 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copright (c) 2017-2019 Gary Klindt 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # task2dot 2 | 3 | This program helps with the creation of visualizations of todo 4 | lists. It works as a simple filter between [taskwarrior](https://github.com/GothenburgBitFactory/taskwarrior) and [graphviz](http://www.graphviz.org/). 5 | 6 | The code is now on https://pypi.org and can be installed via 7 | 8 | python3 -m pip install task2dot 9 | 10 | if you prefer to work with installed software, compared to interpreted source code. 11 | Otherwise execution is performed via python3 task2dot/task2dot.py. 12 | 13 | # usage 14 | 15 | At the command line write 16 | 17 | task export | task2dot | dot -Tsvg > test.svg 18 | 19 | With this, all todo items that you have ever created are fed into 20 | `task2dot`. Without any arguments, it just translates the export 21 | into a format that is suitable for `graphviz` whose output is saved 22 | in the file `test.svg`. We can expect this to result in an insanely 23 | messy network graphics. 24 | 25 | For clarity, I will not show the dot command and the output 26 | redirection into a file in any the following code snippets. Note 27 | that those have to be added for obtaining useful visualizations. 28 | 29 | When exporting data from `taskwarrior` one has to explicitely state 30 | that one only wants to export pending tasks: 31 | 32 | task status:pending export | task2dot 33 | 34 | See a working example: 35 | ![example graph from taskwarrior list](example.png) 36 | 37 | ## what are the nodes and edges 38 | 39 | Tasks, tags and projects are nodes as well as user defined attributes! 40 | User defined attributes are supported if the task configuration file is 41 | `~/.taskrc` or can be found in the environment variable `$TASKRC`. 42 | 43 | Edges are task dependencies and all connections from tasks to their project, 44 | all of their tags as well as the values of their user defined attributes. 45 | 46 | Note that if all node with all of its connections are shown in a graph with 47 | a sufficiently many tasks, the resulting graph will become overwhelmingly messy. 48 | Therefore, the following exclusion mechanisms are available. 49 | 50 | ## node and edge exclusion 51 | 52 | ### node exclusion 53 | 54 | To exclude a specific node write 55 | 56 | task status:pending export | task2dot -node 57 | 58 | Then there will be no node with the content 'node' in the output 59 | graph. 60 | 61 | So why is this useful? If you try to implement *Kanban* or something 62 | similar you are very likely to have a certain tag or a user 63 | defined attribute, like `todo` far too often for having it in a 64 | graph visualization. Almost all tasks would be connected to it via 65 | edges, which is useless. Also, if you export taskwarrior data, 66 | which is filtered with a specific `tag` will cause the resulting 67 | graph having a lot of connections to that tag. So the following 68 | visualization would be useful: 69 | 70 | task status:pending +work | task2dot -work 71 | 72 | ### node type and edge exclusion 73 | 74 | A specific type of node can be excluded by using two hyphens. For 75 | example, not showing any project nodes looks like this: 76 | 77 | task status:pending export | task2dot --project 78 | 79 | Or not showing any tags: 80 | 81 | task status:pending export | task2dot --tags 82 | 83 | In my workflow, paths and emails ids are attached to tasks, so I 84 | need to write 85 | 86 | task export | task2dot --path --email 87 | 88 | When converting tasks with annotations to a graph, by default the 89 | annotations are concatenated and added to the task description. To 90 | turn of annotations altogether, you can use `task2dot --annotation`. 91 | 92 | It is also possible to exclude certain connections also by using 93 | double hyphen. Let's get rid of all connections from tasks to 94 | tags: 95 | 96 | task export | task2dot --task-tags 97 | 98 | ## more connections: overnext neighbors 99 | 100 | It is possible to add additional edges than what taskwarrior 101 | exports directly. One could for example add edges between projects 102 | and tags because they are connected by tasks that have both. If one 103 | then removes the tasks one can look at a graph that shows us which 104 | 'actions' are needed for certain projects, if the tags represent 105 | 'actions'. Similarly to edge exclusion, we use ++node1-node2 to add 106 | additional edges. 107 | 108 | task export | task2dot ++tags-project --task 109 | 110 | # colorscheme configuration 111 | 112 | To adjust the colors to your needs, you will need to provide a file 113 | `~/.task2dotrc.json` in your home directory. It contains a json string 114 | with the following format: 115 | 116 | ``` json 117 | { 118 | "nodes": { 119 | "tags": { 120 | "shape": "egg", 121 | "fillcolor": "#000000", 122 | "penwidth": "2pt", 123 | "style": "filled", 124 | "fontcolor": "white", 125 | "color": "white" 126 | }, 127 | "outcome": { 128 | "shape": "egg", 129 | "fillcolor": "#000000", 130 | "penwidth": "2pt", 131 | "style": "filled", 132 | "fontcolor": "white", 133 | "color": "white" 134 | }, 135 | "people": { 136 | "shape": "octagon", 137 | "style": "filled", 138 | "fillcolor": "#dd9900", 139 | "color": "white" 140 | }, 141 | "project": { 142 | "shape": "diamond", 143 | "penwidth": "2pt", 144 | "color": "#22ff22", 145 | "fontcolor": "#ffffff", 146 | "style": "filled", 147 | "fillcolor": "#115500" 148 | }, 149 | "task": { 150 | "shape": "box", 151 | "color": "white", 152 | "fontcolor": "white", 153 | "style": "rounded,filled", 154 | "fillcolor": "#222299", 155 | "fontsize": "16" 156 | }, 157 | "default": { 158 | "shape": "box", 159 | "color": "white", 160 | "fontcolor": "white", 161 | "style": "rounded,filled", 162 | "fillcolor": "#222299", 163 | "fontsize": "16" 164 | }, 165 | "annotation": { 166 | "shape": "note", 167 | "color": "white", 168 | "fontcolor": "white", 169 | "style": "filled", 170 | "fillcolor": "#111155" 171 | }, 172 | "state": { 173 | "shape": "circle", 174 | "color": "white", 175 | "fontcolor": "white" 176 | } 177 | }, 178 | "edges": { 179 | "default": { 180 | "color": "white" 181 | }, 182 | "task-tags": { 183 | "color": "white", 184 | "style": "dotted", 185 | "penwidth": "5pt", 186 | "arrowsize": "0.1" 187 | }, 188 | "project-project": { 189 | "color": "#22ff22", 190 | "penwidth": "2pt", 191 | "arrowhead": "odot", 192 | "arrowtail": "odot" 193 | }, 194 | "task-project": { 195 | "color": "#aa2211" 196 | } 197 | }, 198 | "graph": { 199 | "layout": "fdp", 200 | "splines": "ortho", 201 | "size": "30,30", 202 | "bgcolor": "#111519" 203 | } 204 | } 205 | ``` 206 | 207 | The parameters that are set in the configuration directly correspond to the 208 | graphviz (see [graphviz](http://www.graphviz.org/)) settings available for nodes, edges and the graph as a whole. The 209 | example configuration shown here corresponds to the color scheme used for 210 | the example graphics referred to from within this README file. 211 | 212 | If you want to provide color settings for your own user defined attribute, just 213 | add a property to the `nodes` property with the name of your attribute and supply the parameters that shall deviate from the `default` configuration. -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garykl/task2dot/a80d20aa96e42d6cee2088d1d7f00828b926816f/example.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="task2dot", 8 | version="0.0.15", 9 | author="Gary Klindt", 10 | author_email="gary.klindt@gmail.com", 11 | description="Convert taskwarrior export to graphviz format and analyse projects", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/garykl/task2dot", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 19 | "Operating System :: Unix", 20 | ], 21 | python_requires='>=3.6', 22 | entry_points = { 23 | 'console_scripts': ['task2dot=task2dot.task2dot:main'] 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /task2dot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garykl/task2dot/a80d20aa96e42d6cee2088d1d7f00828b926816f/task2dot/__init__.py -------------------------------------------------------------------------------- /task2dot/task2dot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # 4 | # Copright 2017 Gary Klindt 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | ################################################################################ 20 | # 21 | # - This script expects json-output from taskwarrior as standard input. 22 | # - Command line arguments are read. 23 | # - All arguments with a leading '-' lead to the exclusion of the specific 24 | # node. 25 | # - All arguments with a leading '--' lead to the exclusion of that node type 26 | # 27 | ################################################################################ 28 | import sys 29 | import os 30 | import subprocess 31 | import json 32 | import textwrap 33 | 34 | 35 | 36 | ################################################################################ 37 | ## 38 | ## extracting the base structure of the tasks 39 | ## 40 | ################################################################################ 41 | 42 | 43 | class Node: 44 | 45 | def __init__(self, kind, label): 46 | self.kind = kind 47 | self.label = label.replace('"', '\\"') 48 | 49 | def __hash__(self): 50 | return hash(self.kind + self.label) 51 | 52 | def __repr__(self): 53 | return "Node('{0}', '{1}')".format(self.kind, self.label) 54 | 55 | def __eq__(self, other): 56 | return hash(self) == hash(other) 57 | 58 | 59 | class Edge: 60 | 61 | def __init__(self, n_1, n_2): 62 | self.node1 = n_1 63 | self.node2 = n_2 64 | 65 | def __eq__(self, other): 66 | return hash(self) == hash(other) 67 | 68 | def __hash__(self): 69 | return hash(str(self.node1) + str(self.node2)) 70 | 71 | def __repr__(self): 72 | return "Edge({0}, {1})".format(self.node1, self.node2) 73 | 74 | def kind(self): 75 | return self.node1 + '-' + self.node2 76 | 77 | 78 | def connector(collections, udas): 79 | """ 80 | generate data structure containing all data 81 | that is necessary for feeding the connections 82 | into the dot program. 83 | 84 | udas is a list UDA types. For each uda, check if there is a value in a task 85 | and save it as an edge. 86 | """ 87 | 88 | 89 | 90 | def task2uda(task, uda): 91 | res = set() 92 | if uda in task.keys(): 93 | for u in task[uda].split(','): 94 | res.add(Edge( 95 | Node('task', task['description']), 96 | Node(uda, u))) 97 | return res 98 | 99 | 100 | def task2task(task): 101 | res = set() 102 | if task['description']: 103 | if 'depends' in task: 104 | 105 | dependencies = task['depends'] 106 | if type(dependencies) is not list: 107 | dependencies = dependencies.split(',') 108 | 109 | for dep in dependencies: 110 | 111 | if dep in collections.uuids: 112 | res.add(Edge( 113 | Node('task', collections.task_dict[dep]['description']), 114 | Node('task', task['description']))) 115 | return res 116 | 117 | 118 | def task2tags(task): 119 | res = set() 120 | if 'tags' in task: 121 | for tag in task['tags']: 122 | if task['status'] != 'deleted': 123 | res.add(Edge( 124 | Node('task', task['description']), 125 | Node('tags', tag))) 126 | return res 127 | 128 | 129 | def task2projects(task, excludedTaskStatus): 130 | res = set() 131 | if task['description']: 132 | if 'project' in task.keys(): 133 | if task['project'] in collections.projects: 134 | if not task['status'] in excludedTaskStatus: 135 | res.add(Edge( 136 | Node('task', task['description']), 137 | Node('project', task['project']))) 138 | return res 139 | 140 | 141 | def project2projects(): 142 | """ 143 | let's support subprojects. 144 | """ 145 | res = set() 146 | 147 | all_projects = set(collections.projects) 148 | for p in collections.projects: 149 | if '.' in p: 150 | all_projects.add(p.split('.')[0]) 151 | 152 | for p1 in all_projects: 153 | for p2 in all_projects: 154 | cond = p1 in p2 and p1 != p2 155 | if cond: 156 | res.add(Edge( 157 | Node('project', p2), 158 | Node('project', p1))) 159 | return res 160 | 161 | 162 | res = set() 163 | res.update(project2projects()) 164 | 165 | for t in collections.tasks: 166 | 167 | res.update(task2task(t)) 168 | res.update(task2tags(t)) 169 | res.update(task2uda(t, 'project')) 170 | 171 | for u in udas: 172 | res.update(task2uda(t, u)) 173 | 174 | return res 175 | 176 | 177 | def filter_nodes(es, nodes): 178 | """ 179 | return edges from es that do not contain nodes from 'nodes'. 180 | """ 181 | res = set() 182 | for e in es: 183 | if e.node1.label in nodes: 184 | continue 185 | if e.node2.label in nodes: 186 | continue 187 | res.add(e) 188 | return res 189 | 190 | 191 | def filter_edges(es, edge_types): 192 | """ 193 | return edges from es that are not of a type in edge_types and do not contain 194 | any nodes of that type. 195 | """ 196 | res = set() 197 | for e in es: 198 | if e.node1.kind in edge_types: 199 | continue 200 | if e.node2.kind in edge_types: 201 | continue 202 | if e.node1.kind + '-' + e.node2.kind in edge_types: 203 | continue 204 | res.add(e) 205 | return res 206 | 207 | 208 | def filter_network(es, nodes, edge_types): 209 | h = filter_nodes(es, nodes) 210 | return filter_edges(h, edge_types) 211 | 212 | 213 | def add_indirect_edges(edges, kind_1, kind_2): 214 | """ 215 | If a node has a connection to 216 | """ 217 | res = set() 218 | 219 | for e_1 in edges: 220 | for e_2 in edges: 221 | 222 | if e_1.node1.kind == kind_1 and e_2.node1.kind == kind_2: 223 | if e_1.node2 == e_2.node2: 224 | res.add(Edge(e_1.node1, e_2.node1)) 225 | if e_1.node1.kind == kind_1 and e_2.node2.kind == kind_2: 226 | if e_1.node2 == e_2.node1: 227 | res.add(Edge(e_1.node1, e_2.node2)) 228 | if e_1.node2.kind == kind_1 and e_2.node1.kind == kind_2: 229 | if e_1.node1 == e_2.node2: 230 | res.add(Edge(e_1.node2, e_2.node1)) 231 | if e_1.node2.kind == kind_1 and e_2.node2.kind == kind_2: 232 | if e_1.node1 == e_2.node1: 233 | res.add(Edge(e_1.node2, e_2.node2)) 234 | 235 | return res 236 | 237 | 238 | 239 | ################################################################################ 240 | ## 241 | ## generate dot format from base structure 242 | ## 243 | ################################################################################ 244 | 245 | 246 | 247 | def generate_dot_source( 248 | connections, node_conf, edge_conf, graph_conf): 249 | """ 250 | node_conf is a dictionary with keys being a possible type of a node. 251 | edge_conf is a dictionary with keys being a possible type of an edge. 252 | The values of the dictionaries are dictionaries with settings. 253 | edges is a list of Edge instances. 254 | """ 255 | 256 | header = "digraph dependencies {" 257 | 258 | for (key, value) in graph_conf.items(): 259 | header += "{0}=\"{1}\"; ".format(key, value) 260 | footer = "}" 261 | 262 | 263 | def node(n): 264 | label = '"' + n.label + '"' 265 | label += '[id="activate(\'{0}\', \'{1}\')"]'.format(n.kind, n.label) 266 | if n.kind in node_conf: 267 | label += "".join(["[{0}=\"{1}\"]".format(k, v) for 268 | (k, v) in node_conf[n.kind].items()]) 269 | else: 270 | label += "".join(["[{0}=\"{1}\"]".format(k, v) for 271 | (k, v) in node_conf['default'].items()]) 272 | return label 273 | 274 | 275 | def edge(e): 276 | line = '"{0}" -> "{1}"'.format( 277 | e.node1.label, 278 | e.node2.label) 279 | kind = e.node1.kind + '-' + e.node2.kind 280 | if kind in edge_conf: 281 | line += "".join(["[{0}=\"{1}\"]".format(k, v) for 282 | (k, v) in edge_conf[kind].items()]) 283 | else: 284 | line += "".join(["[{0}=\"{1}\"]".format(k, v) for 285 | (k, v) in edge_conf['default'].items()]) 286 | return line 287 | 288 | res = [header] 289 | 290 | # edges 291 | for e in connections: 292 | res.append(edge(e)) 293 | res.append(node(e.node1)) 294 | res.append(node(e.node2)) 295 | 296 | res.append(footer) 297 | return "\n".join(res) 298 | 299 | 300 | 301 | ################################################################################ 302 | ## 303 | ## interact with the os environment 304 | ## 305 | ################################################################################ 306 | 307 | 308 | 309 | def json_from_task_process(task_query): 310 | """ 311 | read input from taskwarrior via stdin, 312 | return list of dictionaries. 313 | """ 314 | output = subprocess.check_output(task_query.split(' ')) 315 | tasks = ','.join(str(output, 'utf-8').split('\n')[:-1]) 316 | return json.loads('[' + tasks + "]") 317 | 318 | 319 | def get_udas_from_task_config(): 320 | """ 321 | read udas from configuration file. 322 | """ 323 | udas = set() 324 | 325 | if os.environ.get('TASKRC') is None: 326 | config_file = '{0}/.taskrc'.format(os.environ['HOME']) 327 | else: 328 | config_file = os.environ['TASKRC'] 329 | 330 | with open(config_file, 'r') as rc: 331 | 332 | lines = [line for line in rc.readlines() if 'uda' == line[:3]] 333 | for line in lines: 334 | udas.add(line.split('.')[1]) 335 | 336 | return udas 337 | 338 | 339 | def get_uda_values_from_tasks(tasks, uda): 340 | res = set() 341 | for task in tasks: 342 | if uda in task: 343 | res.add(task[uda]) 344 | return res 345 | 346 | 347 | def get_udas_from_task_process(): 348 | udas = get_udas_from_task_config() 349 | tasks = json_from_task_process('task status:pending export') 350 | return {u: get_uda_values_from_tasks(tasks, u) 351 | for u in udas} 352 | 353 | 354 | def task_with_annotations(task): 355 | 356 | def wrap_text(strng, chars_per_line=25): 357 | lines = textwrap.wrap(strng, width=chars_per_line) 358 | return "\\n".join(lines) 359 | 360 | res = wrap_text(task['description'], chars_per_line=25) + '\n' 361 | if 'annotations' in task: 362 | for anno in task['annotations']: 363 | res += anno['entry'] +':\n' 364 | res += wrap_text(anno['description'], chars_per_line=20) + '\n' 365 | return res 366 | 367 | 368 | class TaskwarriorExploit(object): 369 | 370 | def __init__(self, tasks, suppress_annotations=False): 371 | self.tasks = tasks 372 | if not suppress_annotations: 373 | for t in tasks: 374 | t['description'] = task_with_annotations(t) 375 | self.projects = self.get_projects() 376 | self.tags = self.get_tags() 377 | self.task_dict = self.get_uuids() 378 | self.uuids = self.task_dict.keys() 379 | 380 | 381 | def get_projects(self): 382 | res = set() 383 | for task in self.tasks: 384 | if 'project' in task.keys(): 385 | res.add(task['project']) 386 | return res 387 | 388 | def get_tags(self): 389 | """ 390 | data is list of dictionaries, containing data from 391 | the export function of taskwarrior. 392 | return a set with the keys. 393 | """ 394 | allTags = set() 395 | for task in self.tasks: 396 | if 'tags' in task.keys(): 397 | for tag in task['tags']: 398 | if tag not in allTags: 399 | allTags.add(tag) 400 | return allTags 401 | 402 | def get_uuids(self): 403 | res = {} 404 | for task in self.tasks: 405 | res[task['uuid']] = task 406 | return res 407 | 408 | 409 | 410 | def read_visual_config(): 411 | """ 412 | read configuration file .task2dotrc.json in the home directory that 413 | contains configuration for the visual appearence of the resulting graph 414 | """ 415 | config_file = '{0}/.task2dotrc.json'.format(os.environ['HOME']) 416 | if os.path.isfile(config_file): 417 | with open(config_file, 'r') as handle: 418 | return json.load(handle) 419 | 420 | node_styles = [ 421 | { 422 | 'shape': 'egg', 423 | 'fillcolor': '#000000', 424 | 'penwidth': '2pt', 425 | 'style': 'filled', 426 | 'fontcolor': 'white', 427 | 'color': 'white'}, 428 | { 429 | 'shape': 'octagon', 430 | 'style': 'filled', 431 | 'fillcolor': '#dd9900', 432 | 'color': 'white'}, 433 | { 434 | 'shape': 'diamond', 435 | 'penwidth': '2pt', 436 | 'color': '#22ff22', 437 | 'fontcolor': '#ffffff', 438 | 'style': 'filled', 439 | 'fillcolor': '#115500'}, 440 | { 441 | 'shape': 'box', 442 | 'color': 'white', 443 | 'fontcolor': 'white', 444 | 'style': 'rounded,filled', 445 | 'fillcolor': '#222299', 446 | 'fontsize': '16'}, 447 | { 448 | 'shape': 'note', 449 | 'color': 'white', 450 | 'fontcolor': 'white', 451 | 'style': 'filled', 452 | 'fillcolor': '#111155'}, 453 | { 454 | 'shape': 'circle', 455 | 'color': 'white', 456 | 'fontcolor': 'white'}] 457 | 458 | edge_styles = [ 459 | { 460 | 'color': 'white'}, 461 | { 462 | 'color': 'white', 463 | 'style': 'dotted', 464 | 'penwidth': '5pt', 465 | 'arrowsize': '0.1'}, 466 | { 467 | 'color': '#22ff22', 468 | 'penwidth': '2pt', 469 | 'arrowhead': 'odot', 470 | 'arrowtail': 'odot'}, 471 | { 472 | 'color': '#aa2211'}] 473 | 474 | graph_styles = { 475 | 'layout': 'fdp', 476 | 'splines': 'ortho', 477 | 'size': '30,30', 478 | 'bgcolor': '#111519' 479 | } 480 | 481 | return { 482 | "nodes": { 483 | 'people': node_styles[1], 484 | 'tags': node_styles[0], 485 | 'outcome': node_styles[0], 486 | 'project': node_styles[2], 487 | 'task': node_styles[3], 488 | 'annotation': node_styles[4], 489 | 'state': node_styles[5], 490 | 'default': node_styles[3] 491 | }, 492 | "edges": { 493 | 'default': edge_styles[0], 494 | 'task-tags': edge_styles[1], 495 | 'project-project': edge_styles[2], 496 | 'task-project': edge_styles[3] 497 | }, 498 | "graph": graph_styles 499 | } 500 | 501 | 502 | 503 | 504 | ################################################################################ 505 | ## 506 | ## main program 507 | ## 508 | ################################################################################ 509 | 510 | 511 | 512 | def json_from_task_stdin(): 513 | """ 514 | read input from taskwarrior via stdin, 515 | return list of dictionaries. 516 | """ 517 | taskwarrioroutput = ''.join(sys.stdin.readlines()) 518 | return json.loads(taskwarrioroutput) 519 | 520 | 521 | def exclusion_from_command_line(): 522 | """ 523 | read command line arguments, return exclusion pattern. 524 | """ 525 | arguments = sys.argv[1:] 526 | node_exclusion = [h[1:] for h in arguments if h[0] == '-' and h[1] != '-'] 527 | edge_exclusion = [h[2:] for h in arguments if h[0] == '-' and h[1] == '-'] 528 | edge_addition = [h[2:] for h in arguments if h[0] == '+' and h[1] == '+'] 529 | return (node_exclusion, edge_exclusion, edge_addition) 530 | 531 | 532 | 533 | def main(): 534 | 535 | (nodes_to_be_excluded, edges_to_be_excluded, edge_addition) = exclusion_from_command_line() 536 | 537 | tasks = json_from_task_stdin() 538 | task_data = TaskwarriorExploit(tasks, 'annotation' in edges_to_be_excluded) 539 | edge_data = connector(task_data, get_udas_from_task_config()) 540 | 541 | more_edges = set() 542 | for e_a in edge_addition: 543 | [kind_1, kind_2] = e_a.split('-') 544 | more_edges.update(add_indirect_edges(edge_data, kind_1, kind_2)) 545 | 546 | edge_data = filter_network( 547 | edge_data, nodes_to_be_excluded, edges_to_be_excluded) 548 | 549 | edge_data.update(more_edges) 550 | 551 | visual_config = read_visual_config() 552 | 553 | print( 554 | generate_dot_source( 555 | edge_data, 556 | visual_config["nodes"], 557 | visual_config["edges"], 558 | visual_config["graph"])) 559 | 560 | if __name__ == "__main__": 561 | main() 562 | -------------------------------------------------------------------------------- /upload2pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf task2dot.egg-info 4 | rm -rf dist 5 | rm -rf build 6 | 7 | python3 setup.py sdist bdist_wheel 8 | python3 -m twine upload dist/* --------------------------------------------------------------------------------