Commit 25265e12 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' of github.com:bram85/topydo

parents 12d7edf0 9d0f961b
test/data/* text eol=lf
test/data/*.ics binary
......@@ -4,7 +4,14 @@ python:
- "3.3"
- "3.4"
- "3.5"
- "pypy3"
env:
- GREEN_OPTS=
matrix:
include:
- python: "pypy3"
env: GREEN_OPTS='--processes 1'
install:
- "python -m pip install pip --upgrade"
- "pip install ."
......@@ -15,7 +22,7 @@ install:
- "pip install pylint"
- "pip install codecov"
script:
- "green -vvr"
- "green -vvr $GREEN_OPTS"
- "python -m pylint --errors-only topydo test"
# Cache Dependencies
after_script:
......
0.11
----
* New: `ls` can group items with the `-g` flag, accepting a group expression
(which has the same format as a sort expression). To group items by project,
run: `topydo ls -g project`.
* New: `ls` can print todo items in the Graphviz Dot format, such that
dependencies can be visualized. Use `ls -f dot`, or
`topydo ls -f dot +ProjectA | dot -Tsvg -o projectA.svg` to make a graph for
project A.
* New: Focus and mark colors are customizable in the column UI (thanks to
@colinsullivan).
* New: todo items can be hidden by adding a `h:1` tag (thanks to @MinchinWeb).
* New: an alternative column definition file can be given with the `-l` flag:
`topydo columns -ls /path/to/columns.conf` (thanks to @mruwek).
* Fix: column UI reloads automatically when the todo.txt file was changed
externally.
* Fix: `edit` did not work on some operating systems (e.g. Mac OS X).
* Fix: relative dates were sometimes one day off.
* Fix: Minor importance calculation fix during the weekend for distant mondays
(thanks to @aetherknight).
* Fix: tests were made more deterministic.
* Change: Performance improvements for the column UI, it scales better with
large todo lists.
* Change: temporary files (for editing) will be detected as todo.txt files by
the todo.txt-vim plugin.
0.10.1
------
......
......@@ -20,6 +20,8 @@ def find_version(*file_paths):
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
WATCHDOG = 'watchdog >= 0.8.3'
setup(
name = "topydo",
packages = find_packages(exclude=["test"]),
......@@ -34,9 +36,9 @@ setup(
extras_require = {
':sys_platform=="win32"': ['colorama>=0.2.5'],
':python_version=="3.2"': ['backports.shutil_get_terminal_size>=1.0.0'],
'columns': ['urwid >= 1.3.0'],
'columns': ['urwid >= 1.3.0', WATCHDOG],
'ical': ['icalendar'],
'prompt': ['prompt_toolkit >= 0.53'],
'prompt': ['prompt_toolkit >= 0.53', WATCHDOG],
'test': ['coverage', 'freezegun', 'green', ],
'test:python_version=="3.2"': ['mock'],
},
......
......@@ -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
......@@ -4,3 +4,5 @@ project_color =
context_color =
link_color =
metadata_color =
focus_background_color =
marked_background_color =
......@@ -24,3 +24,5 @@ project_color = junk
context_color = junk
metadata_color = junk
link_color = junk
focus_background_color = junk
marked_background_color = junk
(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
+A only test:test_group1
+B only test:test_group1
+A and +B test:test_group1
No project test:test_group1
Different item test:test_group2 l:1
Another item test:test_group2 l:0
Test 1 due:2016-12-06 test:test_group3
Test 2 due:2016-12-07 test:test_group3
Test 1 t:2016-12-06 test:test_group4 test:test_group5
Test 2 t:2016-12-07 test:test_group4 test:test_group5
Group by non-existing tag test:test_group6
Sort descending +A test:test_group7
Sort descending +B test:test_group7
Inner sort 1 +A @A test:test_group8
Inner sort 2 +A @B test:test_group8
Inner sort 3 +B @A test:test_group8
Inner sort 4 +B @B test:test_group8
Inner sort 1 +A test:test_group9
Inner sort 2 +A test:test_group9
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"]
}
......@@ -5,3 +5,4 @@
(C) 13 + 29 = 42
x 2014-12-12 Completed but with date:2014-12-12
hidden item h:1
(A) Foo
2014-06-14 Last
First
first
......@@ -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
......
......@@ -154,6 +154,14 @@ class ColorsTest(TopydoTest):
self.assertEqual(color_b, '')
self.assertEqual(color_c, '')
def test_focus_color(self):
config(p_overrides={('colorscheme', 'focus_background_color'): 'gray'})
self.assertEqual(config().focus_background_color().as_ansi(), '\033[0;37m')
def test_mark_color(self):
config(p_overrides={('colorscheme', 'marked_background_color'): 'blue'})
self.assertEqual(config().marked_background_color().as_ansi(), '\033[0;34m')
def test_empty_color_values(self):
config("test/data/ColorsTest5.conf")
project_color = config().project_color().as_ansi(p_decoration='bold')
......
......@@ -128,6 +128,18 @@ class ConfigTest(TopydoTest):
self.assertEqual(config("test/data/ConfigTest5.conf").link_color().color, 6)
def test_config24(self):
""" No focus background color value. """
self.assertEqual(config("test/data/ConfigTest5.conf").focus_background_color().color, 7)
def test_config25(self):
""" No mark background color value. """
self.assertEqual(config("test/data/ConfigTest5.conf").marked_background_color().color, 4)
def test_config26(self):
self.assertTrue(config("test/data/ConfigTest4.conf").focus_background_color().is_neutral())
self.assertTrue(config("test/data/ConfigTest4.conf").marked_background_color().is_neutral())
def test_config27(self):
""" column_keymap test. """
keymap, keystates = config("test/data/ConfigTest6.conf").column_keymap()
......
......@@ -295,6 +295,39 @@ class DepCommandTest(CommandTest):
self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n")
def test_dot1(self):
command = DepCommand(["dot"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n")
def test_dot2(self):
self.maxDiff = None
command = DepCommand(["dot", "1"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, """digraph topydo {
node [ shape="none" margin="0" fontsize="9" fontname="Helvetica" ]
_2 [label=<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top"><TR><TD><B>2</B></TD><TD BALIGN="LEFT"><B>Bar</B></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</B></TD></TR></TABLE>> style=filled fillcolor="#008000" fontcolor="#ffffff"]
_1 [label=<<TABLE CELLBORDER="0" CELLSPACING="1" VALIGN="top"><TR><TD><B>1</B></TD><TD BALIGN="LEFT"><B>Foo</B></TD></TR></TABLE>> style=filled fillcolor="#008000" fontcolor="#ffffff"]
_1 -> _2
_1 -> _3
}\n
""")
self.assertEqual(self.errors, "")
def test_dot3(self):
command = DepCommand(["dot", "99"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given." + "\n")
def gc_helper(self, p_subcommand):
command = DepCommand([p_subcommand], self.todolist, self.out,
self.error)
......
......@@ -56,5 +56,39 @@ class ImportanceTest(TopydoTest):
todo = Todo("(C) Foo " + config().tag_due() + ":" + "2015-11-09")
self.assertEqual(importance(todo), 6)
@freeze_time("2016, 10, 21")
class ImportanceWeekendFridayTest(TopydoTest):
def test_importance_ignore_weekends_due_not_next_monday(self):
# Today is friday
# due on a monday, but over a month away.
# So 2 + 0 (no priority) + 0 (no star) + 0 (due > 14 days)
config(p_overrides={('sort', 'ignore_weekends'): '1'})
todo = Todo("Foo " + config().tag_due() + ":" + "2016-11-28")
self.assertEqual(importance(todo), 2)
@freeze_time("2016, 10, 22")
class ImportanceWeekendSaturdayTest(TopydoTest):
def test_importance_ignore_weekends_due_not_next_monday(self):
# Today is saturday
# due on a monday, but over a month away.
# So 2 + 0 (no priority) + 0 (no star) + 0 (due > 14 days)
config(p_overrides={('sort', 'ignore_weekends'): '1'})
todo = Todo("Foo " + config().tag_due() + ":" + "2016-11-28")
self.assertEqual(importance(todo), 2)
@freeze_time("2016, 10, 23")
class ImportanceWeekendSundayTest(TopydoTest):
def test_importance_ignore_weekends_due_not_next_monday(self):
# Today is sunday
# due on a monday, but over a month away.
# So 2 + 0 (no priority) + 0 (no star) + 0 (due > 14 days)
config(p_overrides={('sort', 'ignore_weekends'): '1'})
todo = Todo("Foo " + config().tag_due() + ":" + "2016-11-28")
self.assertEqual(importance(todo), 2)
if __name__ == '__main__':
unittest.main()
......@@ -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
......
This diff is collapsed.
......@@ -33,14 +33,14 @@ class SortCommandTest(CommandTest):
command.execute()
self.assertEqual(self.todolist.print_todos(),
"First\n(A) Foo\n2014-06-14 Last")
"first\n(A) Foo\n2014-06-14 Last")
def test_sort2(self):
command = SortCommand([], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.todolist.print_todos(),
"(A) Foo\n2014-06-14 Last\nFirst")
"(A) Foo\n2014-06-14 Last\nfirst")
def test_sort3(self):
""" Check that order does not influence the UID of a todo. """
......
......@@ -50,6 +50,8 @@ append_parent_contexts = 0
; context_color = magenta
; metadata_color = green
; link_color = light-cyan
; focus_background_color = gray
; marked_background_color = blue
[aliases]
;showall = ls -x
......
......@@ -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,26 @@ class DepCommand(Command):
except InvalidCommandArgument:
self.error(self.usage())
def _handle_dot(self):
""" Handles the dot subsubcommand. """
self.printer = DotPrinter(self.todolist)
try:
arg = self.argument(1)
todo = self.todolist.todo(arg)
arg = self.argument(1)
todos = set([self.todolist.todo(arg)])
todos |= set(self.todolist.children(todo))
todos |= set(self.todolist.parents(todo))
todos = sorted(todos, key=lambda t: t.text())
self.out(self.printer.print_list(todos))
except InvalidTodoException:
self.error("Invalid todo number given.")
except InvalidCommandArgument:
self.error(self.usage())
def execute(self):
if not super().execute():
return False
......@@ -140,6 +161,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 +176,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 +186,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
......
......@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import codecs
import tempfile
from subprocess import CalledProcessError, check_call
......@@ -54,16 +55,16 @@ class EditCommand(MultiCommand):
self.multi_mode = False
def _todos_to_temp(self):
f = tempfile.NamedTemporaryFile()
f = tempfile.NamedTemporaryFile(delete=False, suffix='.todo.txt')
for todo in self.todos:
f.write((todo.source() + "\n").encode('utf-8'))
f.seek(0)
f.close()
return f
def _todos_from_temp(self, p_temp_file):
p_temp_file.seek(0)
todos = p_temp_file.read().decode('utf-8').splitlines()
f = codecs.open(p_temp_file.name, encoding='utf-8')
todos = f.read().splitlines()
todo_objs = []
for todo in todos:
......@@ -121,6 +122,8 @@ class EditCommand(MultiCommand):
else:
self.error(self.usage())
os.unlink(temp_todos.name)
def _execute_not_multi(self):
if self.edit_archive:
archive = config().archive()
......
......@@ -20,11 +20,13 @@ import os
from topydo.lib.Config import config
from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.Filter import InstanceFilter
from topydo.lib.PrettyPrinter import pretty_printer_factory
from topydo.lib.Filter import HiddenTagFilter, InstanceFilter
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.Sorter import Sorter
from topydo.lib.Utils import get_terminal_size
from topydo.lib.View import View
class ListCommand(ExpressionCommand):
......@@ -37,6 +39,7 @@ class ListCommand(ExpressionCommand):
self.printer = None
self.sort_expression = config().sort_string()
self.group_expression = config().group_string()
self.show_all = False
self.ids = None
self.format = config().list_format()
......@@ -55,7 +58,7 @@ class ListCommand(ExpressionCommand):
return True
def _process_flags(self):
opts, args = self.getopt('f:F:i:n:Ns:x')
opts, args = self.getopt('f:F:g:i:n:Ns:x')
for opt, value in opts:
if opt == '-x':
......@@ -64,16 +67,25 @@ 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':
self.format = value
elif opt == '-g':
self.group_expression = value
elif opt == '-N':
# 2 lines are assumed to be taken up by printing the next prompt
# display at least one item
......@@ -93,8 +105,9 @@ class ListCommand(ExpressionCommand):
def _filters(self):
"""
Additional filters to select particular todo items given with the -i
flag.
Additional filters to:
- select particular todo items given with the -i flag,
- hide appropriately tagged items in the absense of the -x flag.
"""
filters = super()._filters()
......@@ -112,6 +125,9 @@ class ListCommand(ExpressionCommand):
todos = [get_todo(i) for i in self.ids]
filters.append(InstanceFilter(todos))
if not self.show_all:
filters.append(HiddenTagFilter())
return filters
def _print(self):
......@@ -132,7 +148,16 @@ class ListCommand(ExpressionCommand):
self.printer = pretty_printer_factory(self.todolist, filters)
self.out(self.printer.print_list(self._view().todos))
if self.group_expression:
self.out(self.printer.print_groups(self._view().groups))
else:
self.out(self.printer.print_list(self._view().todos))
def _view(self):
sorter = Sorter(self.sort_expression, self.group_expression)
filters = self._filters()
return View(sorter, filters, self.todolist)
def _N_lines(self):
''' Determine how many lines to print, such that the number of items
......@@ -179,9 +204,9 @@ class ListCommand(ExpressionCommand):
return True
def usage(self):
return """Synopsis: ls [-x] [-s <SORT EXPRESSION>] [-f <OUTPUT FORMAT>]
[-F <FORMAT STRING>] [-i <NUMBER 1>[,<NUMBER 2> ...]] [-N | -n <INTEGER>]
[EXPRESSION]"""
return """Synopsis: ls [-x] [-s <SORT EXPRESSION>]
[-g <GROUP EXPRESSION>] [-f <OUTPUT FORMAT>] [-F <FORMAT STRING>]
[-i <NUMBER 1>[,<NUMBER 2> ...]] [-N | -n <INTEGER>] [EXPRESSION]"""
def help(self):
return """\
......@@ -193,9 +218,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
......@@ -233,10 +261,14 @@ When an EXPRESSION is given, only the todos matching that EXPRESSION are shown.
(empty string) when an item has no priority set.
A tab character serves as a marker to start right alignment.
-g : Group items according to a GROUP EXPRESSION. A group expression is similar
to a sort expression. Defaults to the group expression in the
configuration.
-i : Comma separated list of todo IDs to print.
-n : Number of items to display. Defaults to the value in the configuration.
-N : Limit number of items displayed such that they fit on the terminal.
-s : Sort the list according to a SORT EXPRESSION. Defaults to the expression
in the configuration.
-x : Show all todos (i.e. do not filter on dependencies or relevance).\
-s : Sort the list according to a SORT EXPRESSION. Defaults to the sort
expression in the configuration.
-x : Show all todos (i.e. do not filter on dependencies, relevance, or hidden
status).\
"""
......@@ -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):
......
......@@ -77,6 +77,7 @@ class _Config:
'ls': {
'hide_tags': 'id,p,ical',
'hidden_item_tags': 'h,hide',
'indent': '0',
'list_limit': '-1',
'list_format': '|%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t',
......@@ -91,6 +92,7 @@ class _Config:
'sort': {
'keep_sorted': '0',
'sort_string': 'desc:importance,due,desc:priority',
'group_string': '',
'ignore_weekends': '1',
},
......@@ -105,6 +107,8 @@ class _Config:
'metadata_color': 'green',
'link_color': 'cyan',
'priority_colors': 'A:cyan,B:yellow,C:blue',
'focus_background_color': 'gray',
'marked_background_color': 'blue'
},
'aliases': {
......@@ -269,6 +273,9 @@ class _Config:
def sort_string(self):
return self.cp.get('sort', 'sort_string')
def group_string(self):
return self.cp.get('sort', 'group_string')
def ignore_weekends(self):
try:
return self.cp.getboolean('sort', 'ignore_weekends')
......@@ -311,6 +318,13 @@ class _Config:
return [] if hidden_tags == '' else [tag.strip() for tag in
hidden_tags.split(',')]
def hidden_item_tags(self):
""" Returns a list of tags which hide an item from the 'ls' output. """
hidden_item_tags = self.cp.get('ls', 'hidden_item_tags')
# pylint: disable=no-member
return [] if hidden_item_tags == '' else [tag.strip() for tag in
hidden_item_tags.split(',')]
def priority_color(self, p_priority):
"""
Returns a dict with priorities as keys and color numbers as value.
......@@ -359,6 +373,18 @@ class _Config:
except ValueError:
return Color(self.cp.get('colorscheme', 'link_color'))
def focus_background_color(self):
try:
return Color(self.cp.getint('colorscheme', 'focus_background_color'))
except ValueError:
return Color(self.cp.get('colorscheme', 'focus_background_color'))
def marked_background_color(self):
try:
return Color(self.cp.getint('colorscheme', 'marked_background_color'))
except ValueError:
return Color(self.cp.get('colorscheme', 'marked_background_color'))
def auto_creation_date(self):
try:
return self.cp.getboolean('add', 'auto_creation_date')
......
......@@ -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
......
......@@ -16,6 +16,7 @@
import re
from topydo.lib.Config import config
from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.Utils import date_string_to_date
......@@ -91,8 +92,8 @@ class RelevanceFilter(Filter):
The item has not been completed AND
The start date is blank, today or in the past, AND
The priority is 'A' or the priority is B with due date within 30 days or
the priority is C with due date within 14 days.
The priority is 'A', or the priority is 'B' with due date within 30 days, or
the priority is 'C' with due date within 14 days.
"""
def match(self, p_todo):
......@@ -154,6 +155,28 @@ class InstanceFilter(Filter):
return False
class HiddenTagFilter(Filter):
def __init__(self):
"""
Constructor.
A filter which hides items marked with the approriate tag.
"""
super().__init__()
def match(self, p_todo):
"""
Returns True when p_todo doesn't have a tag to mark it as hidden.
"""
for my_tag in config().hidden_item_tags():
my_values = p_todo.tag_values(my_tag)
for my_value in my_values:
if not my_value in (0, '0', False, 'False'):
return False
return True
class LimitFilter(Filter):
def __init__(self, p_limit):
super().__init__()
......
......@@ -31,12 +31,14 @@ IMPORTANCE_VALUE = {'A': 3, 'B': 2, 'C': 1}
def is_due_next_monday(p_todo):
""" Returns True when the given task is due next Monday. """
""" Returns True when today is Friday (or the weekend) and the given task
is due next Monday.
"""
today = date.today()
due = p_todo.due_date()
return due and due.weekday() == 0 and today.weekday() >= 4 and \
p_todo.days_till_due()
p_todo.days_till_due() <= 3
def importance(p_todo, p_ignore_weekend=config().ignore_weekends()):
......
......@@ -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.
......
......@@ -16,56 +16,110 @@
""" This module provides functionality to sort lists with todo items. """
from collections import OrderedDict, namedtuple
from itertools import groupby
import re
from datetime import date
from topydo.lib.Config import config
from topydo.lib.Importance import average_importance, importance
from topydo.lib.Utils import humanize_date
def is_priority_field(p_field):
""" Returns True when the field name denotes the priority. """
return p_field.startswith('prio')
Field = namedtuple('Field', ['sort', 'group', 'label'])
def get_field_function(p_field):
"""
Given a property (string) of a todo, return a function that attempts to
access that property. If the property could not be located, return the
identity function.
"""
result = lambda a: a
if is_priority_field(p_field):
# assign dummy priority when a todo has no priority
result = lambda a: a.priority() or 'ZZ'
elif p_field == 'context' or p_field == 'contexts':
result = lambda a: sorted([c.lower() for c in a.contexts()])
elif p_field == 'creationdate' or p_field == 'creation':
FIELDS = {
'completed': Field(
# when a task has no completion date, push it to the end by assigning it
# the maximum possible date.
sort=(lambda t: t.completion_date() if t.completion_date() else date.max),
group=(lambda t: humanize_date(t.completion_date()) if t.completion_date() else 'None'),
label='Completed',
),
'context': Field(
sort=lambda t: sorted(c.lower() for c in t.contexts()) or ['zz'],
group=lambda t: sorted(t.contexts()) or ['None'],
label='Context'
),
'created': Field(
# when a task has no creation date, push it to the end by assigning it
# the maximum possible date.
result = (lambda a: a.creation_date() if a.creation_date()
else date.max)
elif p_field == 'done' or p_field == 'completed' or p_field == 'completion':
result = (lambda a: a.completion_date() if a.completion_date()
else date.max)
elif p_field == 'importance':
result = importance
elif p_field == 'importance-avg' or p_field == 'importance-average':
result = average_importance
elif p_field == 'length':
result = lambda a: a.length()
elif p_field == 'project' or p_field == 'projects':
result = lambda a: sorted([c.lower() for c in a.projects()])
elif p_field == 'text':
result = lambda a: a.text()
else:
# try to find the corresponding tag
# when a tag is not present, push it to the end of the list by giving
# it an artificially higher value
result = (lambda a: "0" + a.tag_value(p_field) if a.has_tag(p_field)
else "1")
return result
sort=(lambda t: t.creation_date() if t.creation_date() else date.max),
group=(lambda t: humanize_date(t.creation_date()) if t.creation_date() else 'None'),
label='Created',
),
'importance': Field(
sort=importance,
group=importance,
label='Importance',
),
'importance-avg': Field(
sort= average_importance,
group=lambda t: round(average_importance(t), 1),
label='Importance (avg)',
),
'length': Field(
sort=lambda t: t.length(),
group=lambda t: t.length(),
label='Length',
),
'priority': Field(
sort=(lambda t: t.priority() or 'ZZ'),
group=(lambda t: t.priority() or 'None'),
label='Priority',
),
'project': Field(
sort=lambda t: sorted(p.lower() for p in t.projects()) or ['zz'],
group=lambda t: sorted(t.projects()) or ['None'],
label='Project',
),
'text': Field(
sort=lambda t: t.text().lower(),
group=lambda t: t.text(),
label='Text',
),
}
# map UI properties to properties in the FIELDS hash
FIELD_MAP = {
'completed': 'completed',
'completion': 'completed',
'completion_date': 'completed',
'done': 'completed',
'context': 'context',
'contexts': 'context',
'created': 'created',
'creation': 'created',
'creation_date': 'created',
'importance': 'importance',
'importance-avg': 'importance-avg',
'importance-average': 'importance-avg',
'length': 'length',
'len': 'length',
'prio': 'priority',
'priorities': 'priority',
'priority': 'priority',
'project': 'project',
'projects': 'project',
'text': 'text',
}
def _apply_sort_functions(p_todos, p_functions):
sorted_todos = p_todos
for function, order in reversed(p_functions):
sorted_todos = sorted(sorted_todos, key=function,
reverse=(order == 'desc'))
return sorted_todos
class Sorter(object):
......@@ -93,10 +147,10 @@ class Sorter(object):
stable.
"""
def __init__(self, p_sortstring="desc:priority"):
self.sortstring = p_sortstring
self.functions = []
self._parse()
def __init__(self, p_sortstring="desc:priority", p_groupstring=""):
self.groupfunctions = self._parse(p_groupstring, p_group=True) if p_groupstring else []
self.pregroupfunctions = self._parse(p_groupstring, p_group=False) if p_groupstring else []
self.sortfunctions = self._parse(p_sortstring, p_group=False)
def sort(self, p_todos):
"""
......@@ -107,19 +161,85 @@ class Sorter(object):
sort operation is done first, relying on the stability of the sorted()
function.
"""
sorted_todos = p_todos
for function, order in reversed(self.functions):
sorted_todos = sorted(sorted_todos, key=function,
reverse=(order == 'desc'))
return _apply_sort_functions(p_todos, self.sortfunctions)
return sorted_todos
def group(self, p_todos):
"""
Groups the todos according to the given group string.
"""
# preorder todos for the group sort
p_todos = _apply_sort_functions(p_todos, self.pregroupfunctions)
# initialize result with a single group
result = OrderedDict([((), p_todos)])
for (function, label), _ in self.groupfunctions:
oldresult = result
result = OrderedDict()
for oldkey, oldgroup in oldresult.items():
for key, _group in groupby(oldgroup, function):
newgroup = list(_group)
if not isinstance(key, list):
key = [key]
for subkey in key:
subkey = "{}: {}".format(label, subkey)
newkey = oldkey + (subkey,)
if newkey in result:
result[newkey] = result[newkey] + newgroup
else:
result[newkey] = newgroup
# sort all groups
for key, _group in result.items():
result[key] = self.sort(_group)
def _parse(self):
return result
def _parse(self, p_string, p_group):
"""
Parses a sort string and returns a list of functions and the
Parses a sort/group string and returns a list of functions and the
desired order.
"""
fields = self.sortstring.lower().split(',')
def get_field_function(p_field, p_group=False):
"""
Turns a field, part of a sort/group string, into a lambda that
takes a todo item and returns the field value.
"""
compose = lambda i: i.sort if not p_group else (i.group, i.label)
def group_value(p_todo):
"""
Returns a value to assign the given todo to a group. Date tags
are grouped according to the relative date (1 day, 1 month,
...)
"""
result = 'No value'
if p_todo.has_tag(p_field):
if p_field == config().tag_due():
result = humanize_date(p_todo.due_date())
elif p_field == config().tag_start():
result = humanize_date(p_todo.start_date())
else:
result = p_todo.tag_value(p_field)
return result
if p_field in FIELD_MAP:
return compose(FIELDS[FIELD_MAP[p_field]])
else:
# treat it as a tag value
return compose(Field(
sort=lambda t: '0' + t.tag_value(p_field) if t.has_tag(p_field) else '1',
group=group_value,
label=p_field,
))
result = []
fields = p_string.lower().split(',')
for field in fields:
parsed_field = re.match(
......@@ -134,11 +254,14 @@ class Sorter(object):
field = parsed_field.group('field')
if field:
function = get_field_function(field)
function = get_field_function(field, p_group)
# reverse order for priority: lower characters have higher
# priority
if is_priority_field(field):
if field in FIELD_MAP and FIELD_MAP[field] == 'priority':
order = 'asc' if order == 'desc' else 'desc'
self.functions.append((function, order))
result.append((function, order))
return result
......@@ -19,6 +19,7 @@ This module deals with todo.txt files.
"""
import codecs
import os.path
class TodoFile(object):
......@@ -28,7 +29,7 @@ class TodoFile(object):
"""
def __init__(self, p_path):
self.path = p_path
self.path = os.path.abspath(p_path)
def read(self):
""" Reads the todo.txt file and returns a list of todo items. """
......@@ -49,6 +50,7 @@ class TodoFile(object):
p_todos can be a list of todo items, or a string that is just written
to the file.
"""
todofile = codecs.open(self.path, 'w', encoding="utf-8")
if p_todos is list:
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2016 Bram Schoenmakers <bram@topydo.org>
#
# 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/>.
"""
This module deals with todo.txt files while putting a watch on them for file
changes.
"""
import os.path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler, FileModifiedEvent, FileCreatedEvent
from topydo.lib.TodoFile import TodoFile
class TodoFileWatched(TodoFile):
"""
This class represents a todo.txt file, which can be read from or written
to.
"""
def __init__(self, p_path, p_on_update):
super().__init__(p_path)
self.self_write = False
class EventHandler(FileSystemEventHandler):
"""
Event handler to catch modifications (or creations) of the
current todo.txt file.
"""
def __init__(self, p_file):
super().__init__()
self.file = p_file
def _handle(self, p_event):
right_type = isinstance(p_event, FileModifiedEvent) or isinstance(p_event, FileCreatedEvent)
should_trigger = right_type and p_event.src_path == self.file.path
if self.file.self_write and should_trigger:
# the file was written by topydo, unmark that so we can
# record external writes again.
self.file.self_write = False
elif should_trigger:
p_on_update()
def on_created(self, p_event):
"""
Because vim deletes and creates a file on buffer save, also
catch a creation event.
"""
self._handle(p_event)
def on_modified(self, p_event):
self._handle(p_event)
observer = Observer()
observer.schedule(EventHandler(self), os.path.dirname(self.path))
observer.start()
def write(self, p_todos):
# make sure not to reread the todo file because this instance is
# actually writing it
self.self_write = True
super().write(p_todos)
......@@ -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(now).replace('just now', 'today')
......@@ -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/>.
""" A view is a list of todos, sorted and filtered. """
""" A view is a list of todos, sorted, grouped and filtered. """
class View(object):
......@@ -29,12 +29,22 @@ class View(object):
self._sorter = p_sorter
self._filters = p_filters
@property
def todos(self):
""" Returns a sorted and filtered list of todos in this view. """
result = self._sorter.sort(self.todolist.todos())
def _apply_filters(self, p_todos):
""" Applies the filters to the list of todo items. """
result = p_todos
for _filter in self._filters:
result = _filter.filter(result)
return result
@property
def todos(self):
""" Returns a sorted and filtered list of todos in this view. """
result = self._sorter.sort(self.todolist.todos())
return self._apply_filters(result)
@property
def groups(self):
result = self._apply_filters(self.todolist.todos())
return self._sorter.group(result)
# 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 sorted(list(children), key=lambda t: t.text()):
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):
......
......@@ -14,6 +14,8 @@
# 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 itertools import chain
from topydo.lib.prettyprinters.Colors import PrettyPrinterColorFilter
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.TopydoString import TopydoString
......@@ -30,8 +32,16 @@ class Printer(object):
raise NotImplementedError
def print_list(self, p_todos):
result = ''
for todo in p_todos:
self.print_todo(todo)
result += self.print_todo(todo)
return result
def print_groups(self, p_groups):
todos = list(chain.from_iterable(p_groups.values()))
return self.print_list(todos)
class PrettyPrinter(Printer):
......@@ -76,6 +86,29 @@ class PrettyPrinter(Printer):
"""
return [self.print_todo(todo) for todo in p_todos]
def print_groups(self, p_groups):
result = []
first = True
def print_header(p_key):
""" Prints a header for the given key. """
if not first:
result.append('')
key_string = ", ".join(p_key)
result.append(key_string)
result.append("=" * len(key_string))
for key, todos in p_groups.items():
if key != ():
# don't print a header for the case that no valid grouping
# could be made (e.g. an invalid group expression)
print_header(key)
first = False
result += self.print_list(todos)
return [TopydoString(s) for s in result]
def pretty_printer_factory(p_todolist, p_additional_filters=None):
""" Returns a pretty printer suitable for the ls and dep subcommands. """
......
......@@ -44,13 +44,13 @@ def main():
from topydo.ui.prompt.Prompt import PromptApplication
PromptApplication().run()
except ImportError:
error("You have to install prompt-toolkit to run prompt mode.")
error("Some additional dependencies for prompt mode were not installed, please install with 'pip install topydo[prompt]'")
elif args[0] == 'columns':
try:
from topydo.ui.columns.Main import UIApplication
UIApplication().run()
except ImportError:
error("You have to install urwid to run column mode.")
error("Some additional dependencies for column mode were not installed, please install with 'pip install topydo[columns]'")
else:
CLIApplication().run()
except IndexError:
......
......@@ -15,10 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import configparser
from os.path import expanduser
from topydo.lib.Config import home_config_path, config
def columns():
def columns(p_alt_layout_path=None):
"""
Returns list with complete column configuration dicts.
"""
......@@ -28,6 +30,7 @@ def columns():
column_dict['title'] = p_cp.get(p_column, 'title')
column_dict['filterexpr'] = p_cp.get(p_column, 'filterexpr')
column_dict['sortexpr'] = p_cp.get(p_column, 'sortexpr')
column_dict['groupexpr'] = p_cp.get(p_column, 'groupexpr')
column_dict['show_all'] = p_cp.getboolean(p_column, 'show_all')
return column_dict
......@@ -36,6 +39,7 @@ def columns():
'title': 'Yet another column',
'filterexpr': '',
'sortexpr': config().sort_string(),
'groupexpr': config().group_string(),
'show_all': '0',
}
......@@ -49,6 +53,8 @@ def columns():
"/etc/topydo_columns.conf",
]
if p_alt_layout_path is not None:
files.insert(0, expanduser(p_alt_layout_path))
for filename in files:
if cp.read(filename):
break
......
......@@ -15,7 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import getopt
import shlex
import sys
import time
import urwid
......@@ -28,12 +30,13 @@ from topydo.lib.Sorter import Sorter
from topydo.lib.Filter import get_filter_list, RelevanceFilter, DependencyFilter
from topydo.lib.Utils import get_terminal_size
from topydo.lib.View import View
from topydo.lib import TodoFile
from topydo.lib.TodoFileWatched import TodoFileWatched
from topydo.lib import TodoList
from topydo.ui.CLIApplicationBase import CLIApplicationBase
from topydo.ui.CLIApplicationBase import CLIApplicationBase, error
from topydo.ui.columns.CommandLineWidget import CommandLineWidget
from topydo.ui.columns.ConsoleWidget import ConsoleWidget
from topydo.ui.columns.KeystateWidget import KeystateWidget
from topydo.ui.columns.TodoWidget import TodoWidget
from topydo.ui.columns.TodoListWidget import TodoListWidget
from topydo.ui.columns.Utils import PaletteItem, to_urwid_color
from topydo.ui.columns.ViewWidget import ViewWidget
......@@ -91,10 +94,28 @@ class UIApplication(CLIApplicationBase):
def __init__(self):
super().__init__()
self._process_flags()
args = self._process_flags()
try:
opts, args = getopt.getopt(args[1:], 'l:')
except getopt.GetoptError as e:
error(str(e))
sys.exit(1)
self.alt_layout_path = None
for opt, value in opts:
if opt == "-l":
self.alt_layout_path = value
def callback():
self.todolist.erase()
self.todolist.add_list(self.todofile.read())
self._update_all_columns()
self._redraw()
self.column_width = config().column_width()
self.todofile = TodoFile.TodoFile(config().todotxt())
self.todofile = TodoFileWatched(config().todotxt(), callback)
self.todolist = TodoList.TodoList(self.todofile.read())
self.marked_todos = []
......@@ -172,18 +193,20 @@ class UIApplication(CLIApplicationBase):
context_color = to_urwid_color(config().context_color())
metadata_color = to_urwid_color(config().metadata_color())
link_color = to_urwid_color(config().link_color())
focus_background_color = to_urwid_color(config().focus_background_color())
marked_background_color = to_urwid_color(config().marked_background_color())
palette = [
(PaletteItem.PROJECT, '', '', '', project_color, ''),
(PaletteItem.PROJECT_FOCUS, '', 'light gray', '', project_color, None),
(PaletteItem.PROJECT_FOCUS, '', 'light gray', '', project_color, focus_background_color),
(PaletteItem.CONTEXT, '', '', '', context_color, ''),
(PaletteItem.CONTEXT_FOCUS, '', 'light gray', '', context_color, None),
(PaletteItem.CONTEXT_FOCUS, '', 'light gray', '', context_color, focus_background_color),
(PaletteItem.METADATA, '', '', '', metadata_color, ''),
(PaletteItem.METADATA_FOCUS, '', 'light gray', '', metadata_color, None),
(PaletteItem.METADATA_FOCUS, '', 'light gray', '', metadata_color, focus_background_color),
(PaletteItem.LINK, '', '', '', link_color, ''),
(PaletteItem.LINK_FOCUS, '', 'light gray', '', link_color, None),
(PaletteItem.DEFAULT_FOCUS, 'black', 'light gray'),
(PaletteItem.MARKED, '', 'light blue'),
(PaletteItem.LINK_FOCUS, '', 'light gray', '', link_color, focus_background_color),
(PaletteItem.DEFAULT_FOCUS, '', 'light gray', '', '', focus_background_color),
(PaletteItem.MARKED, '', 'light blue', '', '', marked_background_color),
]
for C in ascii_uppercase:
......@@ -196,7 +219,7 @@ class UIApplication(CLIApplicationBase):
'pri_' + C, '', '', '', pri_color, ''
))
palette.append((
'pri_' + C + '_focus', '', 'light gray', '', pri_color_focus, None
'pri_' + C + '_focus', '', 'light gray', '', pri_color_focus, focus_background_color
))
return palette
......@@ -220,6 +243,7 @@ class UIApplication(CLIApplicationBase):
def _set_alarm_for_next_midnight_update(self):
def callback(p_loop, p_data):
TodoWidget.wipe_cache()
self._update_all_columns()
self._set_alarm_for_next_midnight_update()
......@@ -299,6 +323,8 @@ class UIApplication(CLIApplicationBase):
self._output if verbosity else lambda _: None)
def _reset_state(self):
for widget in TodoWidget.cache.values():
widget.unmark()
self.marked_todos = []
self._update_all_columns()
......@@ -389,7 +415,7 @@ class UIApplication(CLIApplicationBase):
"""
Converts a dictionary describing a view to an actual UIView instance.
"""
sorter = Sorter(p_data['sortexpr'])
sorter = Sorter(p_data['sortexpr'], p_data['groupexpr'])
filters = []
if not p_data['show_all']:
......@@ -535,13 +561,16 @@ class UIApplication(CLIApplicationBase):
self._console_visible = True
self.console.print_text(p_text)
def _redraw(self):
self.mainloop.draw_screen()
def _input(self, p_question):
self._print_to_console(p_question)
# don't wait for the event loop to enter idle, there is a command
# waiting for input right now, so already go ahead and draw the
# question on screen.
self.mainloop.draw_screen()
self._redraw()
user_input = self.mainloop.screen.get_input()
self._console_visible = False
......@@ -572,7 +601,7 @@ class UIApplication(CLIApplicationBase):
return False
def run(self):
layout = columns()
layout = columns(self.alt_layout_path)
if len(layout) > 0:
for column in layout:
self._add_column(self._viewdata_to_view(column))
......@@ -580,6 +609,7 @@ class UIApplication(CLIApplicationBase):
dummy = {
"title": "All tasks",
"sortexpr": "desc:prio",
"groupexpr": "",
"filterexpr": "",
"show_all": True,
}
......
......@@ -92,10 +92,17 @@ class TodoListWidget(urwid.LineBox):
del self.todolist[:]
for todo in self.view.todos:
todowidget = TodoWidget(todo, self.view.todolist.number(todo))
self.todolist.append(todowidget)
self.todolist.append(urwid.Divider('-'))
for group, todos in self.view.groups.items():
if len(self.view.groups) > 1:
grouplabel = ", ".join(group)
self.todolist.append(urwid.Text(grouplabel))
self.todolist.append(urwid.Divider('-'))
for todo in todos:
todowidget = TodoWidget.create(todo)
todowidget.number = self.view.todolist.number(todo)
self.todolist.append(todowidget)
self.todolist.append(urwid.Divider('-'))
if old_focus_position:
try:
......
......@@ -53,7 +53,7 @@ def _markup(p_todo, p_focus):
class TodoWidget(urwid.WidgetWrap):
def __init__(self, p_todo, p_number):
def __init__(self, p_todo):
# clients use this to associate this widget with the given todo item
self.todo = p_todo
......@@ -87,21 +87,21 @@ class TodoWidget(urwid.WidgetWrap):
else:
txt_markup.append(substring)
id_widget = urwid.Text(str(p_number), align='right')
self.id_widget = urwid.Text('', align='right')
priority_widget = urwid.Text(priority_text)
self.text_widget = urwid.Text(txt_markup)
progress = to_urwid_color(progress_color(p_todo)) if config().colors() else PaletteItem.DEFAULT
progress_bar = urwid.AttrMap(
self.progress_bar = urwid.AttrMap(
urwid.SolidFill(' '),
urwid.AttrSpec(PaletteItem.DEFAULT, progress, 256),
urwid.AttrSpec(PaletteItem.DEFAULT, progress, 256),
{},
)
self.update_progress()
self.columns = urwid.Columns(
[
(1, progress_bar),
(4, id_widget),
(1, self.progress_bar),
(4, self.id_widget),
(3, priority_widget),
('weight', 1, self.text_widget),
],
......@@ -128,6 +128,21 @@ class TodoWidget(urwid.WidgetWrap):
# make sure that ListBox will highlight this widget
return True
@property
def number(self):
pass
@number.setter
def number(self, p_number):
self.id_widget.set_text(str(p_number))
def update_progress(self):
color = to_urwid_color(progress_color(self.todo)) if config().colors() else PaletteItem.DEFAULT
self.progress_bar.set_attr_map(
{None: urwid.AttrSpec(PaletteItem.DEFAULT, color, 256)}
)
def mark(self):
attr_map = {
None: PaletteItem.MARKED,
......@@ -140,3 +155,45 @@ class TodoWidget(urwid.WidgetWrap):
def unmark(self):
self.widget.set_attr_map(_markup(self.todo, False))
cache = {}
@classmethod
def create(p_class, p_todo):
"""
Creates a TodoWidget instance for the given todo. Widgets are
cached, the same object is returned for the same todo item.
"""
def parent_progress_may_have_changed(p_todo):
"""
Returns True when a todo's progress should be updated because it is
dependent on the parent's progress.
"""
return p_todo.has_tag('p') and not p_todo.has_tag('due')
source = p_todo.source()
if source in p_class.cache:
widget = p_class.cache[source]
if p_todo is not widget.todo:
# same source text but different todo instance (could happen
# after an edit where a new Todo instance is created with the
# same text as before)
# simply fix the reference in the stored widget.
widget.todo = p_todo
if parent_progress_may_have_changed(p_todo):
widget.update_progress()
else:
widget = p_class(p_todo)
p_class.cache[source] = widget
return widget
@classmethod
def wipe_cache(p_class):
""" Wipes the cache """
p_class.cache = {}
......@@ -24,16 +24,18 @@ class ViewWidget(urwid.LineBox):
self.titleedit = urwid.Edit("Title: ", "")
self.sortedit = urwid.Edit("Sort expression: ", "")
self.groupedit = urwid.Edit("Group expression: ", "")
self.filteredit = urwid.Edit("Filter expression: ", "")
group = []
self.relevantradio = urwid.RadioButton(group, "Only show relevant todo items", True)
self.allradio = urwid.RadioButton(group, "Show all todo items")
radiogroup = []
self.relevantradio = urwid.RadioButton(radiogroup, "Only show relevant todo items", True)
self.allradio = urwid.RadioButton(radiogroup, "Show all todo items")
self.pile = urwid.Pile([
self.filteredit,
self.titleedit,
self.sortedit,
self.groupedit,
self.relevantradio,
self.allradio,
urwid.Button("Save", lambda _: urwid.emit_signal(self, 'save')),
......@@ -51,6 +53,7 @@ class ViewWidget(urwid.LineBox):
return {
'title': self.titleedit.edit_text or self.filteredit.edit_text,
'sortexpr': self.sortedit.edit_text or config().sort_string(),
'groupexpr': self.groupedit.edit_text or config().group_string(),
'filterexpr': self.filteredit.edit_text,
'show_all': self.allradio.state,
}
......@@ -59,6 +62,7 @@ class ViewWidget(urwid.LineBox):
def data(self, p_data):
self.titleedit.edit_text = p_data['title']
self.sortedit.edit_text = p_data['sortexpr']
self.groupedit.edit_text = p_data['groupexpr']
self.filteredit.edit_text = p_data['filterexpr']
self.relevantradio.set_state(not p_data['show_all'])
self.allradio.set_state(p_data['show_all'])
......
......@@ -37,21 +37,10 @@ except ConfigError as config_error:
sys.exit(1)
from topydo.Commands import get_subcommand
from topydo.lib import TodoFile
from topydo.lib.TodoFileWatched import TodoFileWatched
from topydo.lib import TodoList
def _todotxt_mtime():
"""
Returns the mtime for the configured todo.txt file.
"""
try:
return os.path.getmtime(config().todotxt())
except os.error:
# file not found
return None
class PromptApplication(CLIApplicationBase):
"""
This class implements a variant of topydo's CLI showing a shell and
......@@ -62,33 +51,25 @@ class PromptApplication(CLIApplicationBase):
super().__init__()
self._process_flags()
self.mtime = None
self.completer = None
self.todofile = TodoFileWatched(config().todotxt(), self._load_file)
def _load_file(self):
"""
Reads the configured todo.txt file and loads it into the todo list
instance.
If the modification time of the todo.txt file is equal to the last time
it was checked, nothing will be done.
"""
current_mtime = _todotxt_mtime()
if not self.todofile or self.mtime != current_mtime:
self.todofile = TodoFile.TodoFile(config().todotxt())
self.todolist = TodoList.TodoList(self.todofile.read())
self.mtime = current_mtime
self.completer = TopydoCompleter(self.todolist)
self.todolist.erase()
self.todolist.add_list(self.todofile.read())
self.completer = TopydoCompleter(self.todolist)
def run(self):
""" Main entry function. """
history = InMemoryHistory()
self._load_file()
while True:
# (re)load the todo.txt file (only if it has been modified)
self._load_file()
try:
user_input = prompt(u'topydo> ', history=history,
......@@ -103,20 +84,12 @@ class PromptApplication(CLIApplicationBase):
error('Error: ' + str(verr))
continue
mtime_after = _todotxt_mtime()
try:
(subcommand, args) = get_subcommand(user_input)
except ConfigError as ce:
error('Error: ' + str(ce) + '. Check your aliases configuration')
continue
# refuse to perform operations such as 'del' and 'do' if the
# todo.txt file has been changed in the background.
if subcommand and not self.is_read_only(subcommand) and self.mtime != mtime_after:
error("WARNING: todo.txt file was modified by another application.\nTo prevent unintended changes, this operation was not executed.")
continue
try:
if self._execute(subcommand, args) != False:
self._post_execute()
......
[all]
title = All tasks
filterexpr =
groupexpr =
[today]
title = Due today
......
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