├── .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 | 
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/*
--------------------------------------------------------------------------------