Commit 7cdcce98 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'dot'

parents 0c87872f bc08f745
test/data/* text eol=lf
test/data/*.ics binary
......@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from test.topydo_testcase import TopydoTest
from topydo.lib.Utils import escape_ansi
......@@ -27,12 +28,12 @@ class CommandTest(TopydoTest):
def out(self, p_output):
if isinstance(p_output, list) and p_output:
self.output += escape_ansi(
"\n".join([str(s) for s in p_output]) + "\n")
os.linesep.join([str(s) for s in p_output]) + os.linesep)
elif p_output:
self.output += str(p_output) + "\n"
self.output += str(p_output) + os.linesep
def error(self, p_error):
if isinstance(p_error, list) and p_error:
self.errors += escape_ansi(p_error + "\n") + "\n"
self.errors += escape_ansi(p_error + os.linesep) + os.linesep
elif p_error:
self.errors += str(p_error) + "\n"
self.errors += str(p_error) + os.linesep
(C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project due:2016-11-18 t:2016-11-17
(D) Bar @Context1 +Project2 p:1
(C) Baz @Context1 +Project1 key:value id:1
(C) Drink beer @ home
(C) 13 + 29 = 42
x 2014-12-12 Completed but with date:2014-12-12
digraph topydo {
node [ shape="none" margin="0" fontsize="9" fontname="Helvetica" ]
_1 [label=<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top"><TR><TD><B>1</B></TD><TD BALIGN="LEFT"><B>Foo @Context2 Not@Context +Project1<BR />Not+Project</B></TD></TR><HR/><TR><TD ALIGN="RIGHT">Prio:</TD><TD ALIGN="LEFT">C</TD></TR><TR><TD ALIGN="RIGHT">Starts:</TD><TD ALIGN="LEFT">2016-11-17 (today)</TD></TR><TR><TD ALIGN="RIGHT">Due:</TD><TD ALIGN="LEFT">2016-11-18 (in a day)</TD></TR></TABLE>> style=filled fillcolor="#008000" fontcolor="#ffffff"]
_3 [label=<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top"><TR><TD><B>3</B></TD><TD BALIGN="LEFT"><B>Baz @Context1 +Project1</B></TD></TR><HR/><TR><TD ALIGN="RIGHT">Prio:</TD><TD ALIGN="LEFT">C</TD></TR></TABLE>> style=filled fillcolor="#008000" fontcolor="#ffffff"]
_4 [label=<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top"><TR><TD><B>4</B></TD><TD BALIGN="LEFT"><B>Drink beer @ home</B></TD></TR><HR/><TR><TD ALIGN="RIGHT">Prio:</TD><TD ALIGN="LEFT">C</TD></TR></TABLE>> style=filled fillcolor="#008000" fontcolor="#ffffff"]
_5 [label=<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top"><TR><TD><B>5</B></TD><TD BALIGN="LEFT"><B>13 + 29 = 42</B></TD></TR><HR/><TR><TD ALIGN="RIGHT">Prio:</TD><TD ALIGN="LEFT">C</TD></TR></TABLE>> style=filled fillcolor="#008000" fontcolor="#ffffff"]
_2 [label=<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top"><TR><TD><B>2</B></TD><TD BALIGN="LEFT"><B>Bar @Context1 +Project2</B></TD></TR><HR/><TR><TD ALIGN="RIGHT">Prio:</TD><TD ALIGN="LEFT">D</TD></TR></TABLE>> style=filled fillcolor="#008000" fontcolor="#ffffff"]
_6 [label=<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top"><TR><TD><B>6</B></TD><TD BALIGN="LEFT"><B><S>Completed but with</S></B></TD></TR></TABLE>> style=filled fillcolor="#008000" fontcolor="#ffffff"]
_3 -> _2
_1 -> _4 [style="invis"]
_4 -> _5 [style="invis"]
_5 -> _6 [style="invis"]
}
......@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.printers.PrettyPrinter import PrettyPrinter
from topydo.lib.Todo import Todo
from topydo.lib.TodoFile import TodoFile
from topydo.lib.TodoList import TodoList
......
......@@ -17,7 +17,7 @@
import unittest
from test.topydo_testcase import TopydoTest
from topydo.lib.JsonPrinter import JsonPrinter
from topydo.lib.printers.Json import JsonPrinter
from topydo.lib.Todo import Todo
......
......@@ -20,6 +20,7 @@ import os
import sys
import unittest
from collections import namedtuple
from freezegun import freeze_time
from test.command_testcase import CommandTest
from test.facilities import load_file_to_todolist
......@@ -527,5 +528,29 @@ class ListCommandIcalTest(CommandTest):
replace_ical_tags(icaltext))
self.assertEqual(self.errors, "")
@freeze_time('2016, 11, 17')
class ListCommandDotTest(CommandTest):
def setUp(self):
self.maxDiff = None
def test_dot(self):
todolist = load_file_to_todolist("test/data/ListCommandDotTest.txt")
command = ListCommand(["-x", "-f", "dot"], todolist, self.out,
self.error)
command.execute()
self.assertFalse(todolist.dirty)
dottext = ""
with codecs.open('test/data/ListCommandTest.dot', 'r',
encoding='utf-8') as dot:
dottext = dot.read()
self.assertEqual(self.output, dottext)
self.assertEqual(self.errors, "")
if __name__ == '__main__':
unittest.main()
......@@ -17,7 +17,8 @@
from topydo.lib import Filter
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.Config import config
from topydo.lib.PrettyPrinter import pretty_printer_factory
from topydo.lib.printers.Dot import DotPrinter
from topydo.lib.printers.PrettyPrinter import pretty_printer_factory
from topydo.lib.Sorter import Sorter
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.View import View
......@@ -130,6 +131,23 @@ class DepCommand(Command):
except InvalidCommandArgument:
self.error(self.usage())
def _handle_dot(self):
""" Handles the dot subsubcommand. """
self.printer = DotPrinter(self.todolist)
arg = self.argument(1)
try:
todo = self.todolist.todo(arg)
todos = set([self.todolist.todo(arg)])
todos |= set(self.todolist.children(todo))
todos |= set(self.todolist.parents(todo))
self.out(self.printer.print_list(todos))
except InvalidTodoException:
self.error("Invalid todo number given.")
def execute(self):
if not super().execute():
return False
......@@ -140,6 +158,7 @@ class DepCommand(Command):
'del': self._handle_rm,
'ls': self._handle_ls,
'clean': self.todolist.clean_dependencies,
'dot': self._handle_dot,
'gc': self.todolist.clean_dependencies,
}
......@@ -154,6 +173,7 @@ class DepCommand(Command):
dep add <NUMBER> <before|partof|after|parents-of|children-of> <NUMBER>
dep ls <NUMBER> to
dep ls to <NUMBER>
dep dot <NUMBER>
dep clean"""
def help(self):
......@@ -163,5 +183,6 @@ class DepCommand(Command):
item 1.
* rm (alias: del) : Removes a dependency.
* ls : Lists all dependencies to or from a certain todo.
* dot : Prints a dependency tree as a Dot graph.
* clean (alias: gc) : Removes redundant id or p tags.\
"""
......@@ -17,7 +17,7 @@
from datetime import date
from topydo.lib.DCommand import DCommand
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.printers.PrettyPrinter import PrettyPrinter
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.Recurrence import NoRecurrenceException, advance_recurring_todo
from topydo.lib.RelativeDate import relative_date_to_date
......
......@@ -21,7 +21,7 @@ import os
from topydo.lib.Config import config
from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.Filter import HiddenTagFilter, InstanceFilter
from topydo.lib.PrettyPrinter import pretty_printer_factory
from topydo.lib.printers.PrettyPrinter import pretty_printer_factory
from topydo.lib.prettyprinters.Format import PrettyPrinterFormatFilter
from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.Utils import get_terminal_size
......@@ -64,12 +64,19 @@ class ListCommand(ExpressionCommand):
self.sort_expression = value
elif opt == '-f':
if value == 'json':
from topydo.lib.JsonPrinter import JsonPrinter
from topydo.lib.printers.Json import JsonPrinter
self.printer = JsonPrinter()
elif value == 'ical':
if self._poke_icalendar():
from topydo.lib.IcalPrinter import IcalPrinter
from topydo.lib.printers.Ical import IcalPrinter
self.printer = IcalPrinter(self.todolist)
elif value == 'dot':
from topydo.lib.printers.Dot import DotPrinter
self.printer = DotPrinter(self.todolist)
# a graph without dependencies is not so useful, hence
# show all
self.show_all = True
else:
self.printer = None
elif opt == '-F':
......@@ -197,9 +204,12 @@ Lists all relevant todos. A todo is relevant when:
When an EXPRESSION is given, only the todos matching that EXPRESSION are shown.
-f : Specify the OUTPUT FORMAT, being 'text' (default), 'ical' or 'json'.
-f : Specify the OUTPUT format, being 'text' (default), 'dot' or 'ical' or
'json'.
* 'text' - Text output with colors and indentation if applicable.
* 'dot' - Prints a dependency graph for the selected items in GraphViz
Dot format.
* 'ical' - iCalendar (RFC 2445). Is not supported in Python 3.2. Be aware
that this is not a read-only operation, todo items may obtain
an 'ical' tag with a unique ID. Completed todo items may be
......
......@@ -45,6 +45,62 @@ class Color:
'white': 15,
}
# Source: https://gist.github.com/jasonm23/2868981
html_color_dict = {
0: "#000000", 1: "#800000", 2: "#008000", 3: "#808000", 4: "#000080",
5: "#800080", 6: "#008080", 7: "#c0c0c0", 8: "#808080", 9: "#ff0000",
10: "#00ff00", 11: "#ffff00", 12: "#0000ff", 13: "#ff00ff", 14: "#00ffff",
15: "#ffffff", 16: "#000000", 17: "#00005f", 18: "#000087", 19: "#0000af",
20: "#0000d7", 21: "#0000ff", 22: "#005f00", 23: "#005f5f", 24: "#005f87",
25: "#005faf", 26: "#005fd7", 27: "#005fff", 28: "#008700", 29: "#00875f",
30: "#008787", 31: "#0087af", 32: "#0087d7", 33: "#0087ff", 34: "#00af00",
35: "#00af5f", 36: "#00af87", 37: "#00afaf", 38: "#00afd7", 39: "#00afff",
40: "#00d700", 41: "#00d75f", 42: "#00d787", 43: "#00d7af", 44: "#00d7d7",
45: "#00d7ff", 46: "#00ff00", 47: "#00ff5f", 48: "#00ff87", 49: "#00ffaf",
50: "#00ffd7", 51: "#00ffff", 52: "#5f0000", 53: "#5f005f", 54: "#5f0087",
55: "#5f00af", 56: "#5f00d7", 57: "#5f00ff", 58: "#5f5f00", 59: "#5f5f5f",
60: "#5f5f87", 61: "#5f5faf", 62: "#5f5fd7", 63: "#5f5fff", 64: "#5f8700",
65: "#5f875f", 66: "#5f8787", 67: "#5f87af", 68: "#5f87d7", 69: "#5f87ff",
70: "#5faf00", 71: "#5faf5f", 72: "#5faf87", 73: "#5fafaf", 74: "#5fafd7",
75: "#5fafff", 76: "#5fd700", 77: "#5fd75f", 78: "#5fd787", 79: "#5fd7af",
80: "#5fd7d7", 81: "#5fd7ff", 82: "#5fff00", 83: "#5fff5f", 84: "#5fff87",
85: "#5fffaf", 86: "#5fffd7", 87: "#5fffff", 88: "#870000", 89: "#87005f",
90: "#870087", 91: "#8700af", 92: "#8700d7", 93: "#8700ff", 94: "#875f00",
95: "#875f5f", 96: "#875f87", 97: "#875faf", 98: "#875fd7", 99: "#875fff",
100: "#878700", 101: "#87875f", 102: "#878787", 103: "#8787af", 104: "#8787d7",
105: "#8787ff", 106: "#87af00", 107: "#87af5f", 108: "#87af87", 109: "#87afaf",
110: "#87afd7", 111: "#87afff", 112: "#87d700", 113: "#87d75f", 114: "#87d787",
115: "#87d7af", 116: "#87d7d7", 117: "#87d7ff", 118: "#87ff00", 119: "#87ff5f",
120: "#87ff87", 121: "#87ffaf", 122: "#87ffd7", 123: "#87ffff", 124: "#af0000",
125: "#af005f", 126: "#af0087", 127: "#af00af", 128: "#af00d7", 129: "#af00ff",
130: "#af5f00", 131: "#af5f5f", 132: "#af5f87", 133: "#af5faf", 134: "#af5fd7",
135: "#af5fff", 136: "#af8700", 137: "#af875f", 138: "#af8787", 139: "#af87af",
140: "#af87d7", 141: "#af87ff", 142: "#afaf00", 143: "#afaf5f", 144: "#afaf87",
145: "#afafaf", 146: "#afafd7", 147: "#afafff", 148: "#afd700", 149: "#afd75f",
150: "#afd787", 151: "#afd7af", 152: "#afd7d7", 153: "#afd7ff", 154: "#afff00",
155: "#afff5f", 156: "#afff87", 157: "#afffaf", 158: "#afffd7", 159: "#afffff",
160: "#d70000", 161: "#d7005f", 162: "#d70087", 163: "#d700af", 164: "#d700d7",
165: "#d700ff", 166: "#d75f00", 167: "#d75f5f", 168: "#d75f87", 169: "#d75faf",
170: "#d75fd7", 171: "#d75fff", 172: "#d78700", 173: "#d7875f", 174: "#d78787",
175: "#d787af", 176: "#d787d7", 177: "#d787ff", 178: "#dfaf00", 179: "#dfaf5f",
180: "#dfaf87", 181: "#dfafaf", 182: "#dfafdf", 183: "#dfafff", 184: "#dfdf00",
185: "#dfdf5f", 186: "#dfdf87", 187: "#dfdfaf", 188: "#dfdfdf", 189: "#dfdfff",
190: "#dfff00", 191: "#dfff5f", 192: "#dfff87", 193: "#dfffaf", 194: "#dfffdf",
195: "#dfffff", 196: "#ff0000", 197: "#ff005f", 198: "#ff0087", 199: "#ff00af",
200: "#ff00df", 201: "#ff00ff", 202: "#ff5f00", 203: "#ff5f5f", 204: "#ff5f87",
205: "#ff5faf", 206: "#ff5fdf", 207: "#ff5fff", 208: "#ff8700", 209: "#ff875f",
210: "#ff8787", 211: "#ff87af", 212: "#ff87df", 213: "#ff87ff", 214: "#ffaf00",
215: "#ffaf5f", 216: "#ffaf87", 217: "#ffafaf", 218: "#ffafdf", 219: "#ffafff",
220: "#ffdf00", 221: "#ffdf5f", 222: "#ffdf87", 223: "#ffdfaf", 224: "#ffdfdf",
225: "#ffdfff", 226: "#ffff00", 227: "#ffff5f", 228: "#ffff87", 229: "#ffffaf",
230: "#ffffdf", 231: "#ffffff", 232: "#080808", 233: "#121212", 234: "#1c1c1c",
235: "#262626", 236: "#303030", 237: "#3a3a3a", 238: "#444444", 239: "#4e4e4e",
240: "#585858", 241: "#626262", 242: "#6c6c6c", 243: "#767676", 244: "#808080",
245: "#8a8a8a", 246: "#949494", 247: "#9e9e9e", 248: "#a8a8a8", 249: "#b2b2b2",
250: "#bcbcbc", 251: "#c6c6c6", 252: "#d0d0d0", 253: "#dadada", 254: "#e4e4e4",
255: "#eeeeee",
}
def __init__(self, p_value=None):
""" p_value is user input, be it a word color or an xterm code """
self._value = None
......@@ -117,3 +173,21 @@ class Color:
color
)
def as_html(self):
try:
return Color.html_color_dict[self.color]
except KeyError:
return '#ffffff'
def as_rgb(self):
"""
Returns a tuple (r, g, b) of the color.
"""
html = self.as_html()
return (
int(html[1:3], 16),
int(html[3:5], 16),
int(html[5:7], 16)
)
......@@ -16,7 +16,7 @@
import getopt
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.printers.PrettyPrinter import PrettyPrinter
class InvalidCommandArgument(Exception):
......
......@@ -17,7 +17,7 @@
import re
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.printers.PrettyPrinter import PrettyPrinter
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
......
......@@ -21,7 +21,7 @@ import re
from topydo.lib.Config import config
from topydo.lib.ProgressColor import progress_color
from topydo.lib.Utils import get_terminal_size, escape_ansi
from topydo.lib.Utils import get_terminal_size, escape_ansi, humanize_date
MAIN_PATTERN = (r'^({{(?P<before>.+?)}})?'
r'(?P<placeholder>{ph}|\[{ph}\])'
......@@ -39,12 +39,6 @@ def _filler(p_str, p_len):
to_fill = p_len - len(p_str)
return to_fill*' ' + p_str
def humanize_date(p_datetime):
""" Returns a relative date string from a datetime object. """
now = arrow.now()
date = now.replace(day=p_datetime.day, month=p_datetime.month, year=p_datetime.year)
return date.humanize().replace('just now', 'today')
def humanize_dates(p_due=None, p_start=None, p_creation=None):
"""
Returns string with humanized versions of p_due, p_start and p_creation.
......
......@@ -24,7 +24,7 @@ from datetime import date
from topydo.lib import Filter
from topydo.lib.Config import config
from topydo.lib.HashListValues import hash_list_values
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.printers.PrettyPrinter import PrettyPrinter
from topydo.lib.Todo import Todo
from topydo.lib.View import View
......
......@@ -18,6 +18,7 @@
Various utility functions.
"""
import arrow
import re
from collections import namedtuple
......@@ -109,3 +110,10 @@ def translate_key_to_config(p_key):
key = p_key
return key
def humanize_date(p_datetime):
""" Returns a relative date string from a datetime object. """
now = arrow.now()
date = now.replace(day=p_datetime.day, month=p_datetime.month, year=p_datetime.year)
return date.humanize().replace('just now', 'today')
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Provides a printer that transforms a list of Todo items to a graph in Dot
notation. Useful for displaying dependencies.
"""
from textwrap import wrap
from topydo.lib.printers.PrettyPrinter import Printer
from topydo.lib.ProgressColor import progress_color
from topydo.lib.Utils import humanize_date
class DotPrinter(Printer):
"""
A printer that converts a list of Todo items to a string in Dot format.
"""
def __init__(self, p_todolist):
super(DotPrinter, self).__init__()
self.todolist = p_todolist
def print_list(self, p_todos):
def node_label(p_todo):
"""
Prints an HTML table for a node label with some todo details.
"""
node_result = '<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top">'
def print_row(p_value1, p_value2):
return '<TR><TD ALIGN="RIGHT">{}</TD><TD ALIGN="LEFT">{}</TD></TR>'.format(p_value1, p_value2)
node_result += '<TR><TD><B>{}</B></TD><TD BALIGN="LEFT"><B>{}{}{}</B></TD></TR>'.format(
self.todolist.number(p_todo),
"<S>" if todo.is_completed() else "",
"<BR />".join(wrap(p_todo.text(), 35)),
"</S>" if todo.is_completed() else "",
)
priority = p_todo.priority()
start_date = p_todo.start_date()
due_date = p_todo.due_date()
if priority or start_date or due_date:
node_result += '<HR/>'
if priority:
node_result += print_row('Prio:', p_todo.priority())
if start_date:
node_result += print_row('Starts:', "{} ({})".format(
start_date.isoformat(),
humanize_date(start_date)
))
if due_date:
node_result += print_row('Due:', "{} ({})".format(
due_date.isoformat(),
humanize_date(due_date)
))
node_result += '</TABLE>>'
return node_result
def foreground(p_background):
"""
Chooses a suitable foreground color (black or white) given a
background color.
"""
(r, g, b) = p_background.as_rgb()
brightness = (r * 299 + g * 587 + b * 114) / ( 255 * 1000 )
return '#ffffff' if brightness < 0.5 else '#000000'
node_name = lambda t: '_' + str(self.todolist.number(t))
result = 'digraph topydo {\n'
result += 'node [ shape="none" margin="0" fontsize="9" fontname="Helvetica" ]\n';
# print todos
for todo in p_todos:
background_color = progress_color(todo)
result += ' {} [label={} style=filled fillcolor="{}" fontcolor="{}"]\n'.format(
node_name(todo),
node_label(todo),
background_color.as_html(),
foreground(background_color),
)
# print edges
for todo in p_todos:
# only print the children that are actually in the list of todos
children = set(p_todos) & set(self.todolist.children(todo,
p_only_direct=True))
for child in children:
result += ' {} -> {}\n'.format(
node_name(todo),
node_name(child)
)
todos_without_dependencies = [todo for todo in p_todos if not self.todolist.children(todo) and not self.todolist.parents(todo)]
for index in range(0, len(todos_without_dependencies) - 1):
this_todo = todos_without_dependencies[index]
next_todo = todos_without_dependencies[index + 1]
result += ' {} -> {} [style="invis"]\n'.format(node_name(this_todo), node_name(next_todo))
result += '}\n'
return result
......@@ -23,7 +23,7 @@ import random
import string
from datetime import datetime, time
from topydo.lib.PrettyPrinter import Printer
from topydo.lib.printers.PrettyPrinter import Printer
def _convert_priority(p_priority):
......
......@@ -21,7 +21,7 @@ such that other applications can process it.
import json
from topydo.lib.PrettyPrinter import Printer
from topydo.lib.printers.PrettyPrinter import Printer
def _convert_todo(p_todo):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment