Commit 2fe9110c authored by Bram Schoenmakers's avatar Bram Schoenmakers

Introduction of color blocks that produce a color based on an attribute

A color block is basically a space with a background color and can be
inserted with a special placeholder for list formatting (not in this
commit). The color is determined by attributes of the todo. This
implementation focusses on the due date.

A color is assigned on a range from green to yellow to red, where red is
overdue, yellow is nearly overdue and green is still 'safe'.

This is relative to the length of the todo item (due minus start date).
The start date can be determined in multiple ways:

* an explicit start date with the t: tag
* the due date minus the recurrence period (rec: tag)

The recurrence takes precedence over the explicit start date in
calculating the length. The primary reason is that t: is mainly used for
filtering purposes.

This is still experimental, I'm just pushing my work in progress here.

TODO:

(B) Add tests
(C) Determine color based on other attributes such as importance,
    priority or a value of a user-specified tag
(C) Revisit the Colors class to fit nicely in the design
parent 2753f348
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 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/>.
from topydo.lib.Colors import int_to_ansi, NEUTRAL_COLOR
from topydo.lib.Recurrence import relative_date_to_date
COLOR16_RANGE = [
10, # light green
2, # green
3, # yellow
1, # red
]
# https://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg
# a gradient from green to yellow to red
COLOR256_RANGE = \
[22, 28, 34, 40, 46, 82, 118, 154, 190, 226, 220, 214, 208, 202, 196]
def progress_color_code(p_todo, p_safe=True):
def get_length():
"""
Returns the length of the p_todo item in days, based on the recurrence
period + due date, or the start/due date.
"""
result = 0
if p_todo.has_tag('rec') and p_todo.due_date():
# add negation, offset is based on due date
neg_recurrence_pattern = '-' + p_todo.tag_value('rec')
start = relative_date_to_date(
neg_recurrence_pattern, p_todo.due_date())
due = p_todo.due_date()
if start < due:
diff = due - start
result = diff.days
else:
result = p_todo.length()
return result
def get_progress():
"""
Returns a value from 0 to 1 where we are today in a date range. Returns
a value >1 when a todo item is overdue.
"""
if p_todo.is_overdue():
return 1.1
elif p_todo.due_date():
days_till_due = p_todo.days_till_due()
length = get_length() or 14
return max((length - days_till_due), 0) / length
else:
return 0
def progress_to_color():
color_range = COLOR16_RANGE if p_safe else COLOR256_RANGE
progress = get_progress()
# TODO: remove linear scale to exponential scale
if progress > 1:
# overdue, return the last color
return color_range[-1]
else:
# not overdue, calculate position over color range excl. due date
# color
pos = round(progress * (len(color_range) - 2))
return color_range[pos]
return progress_to_color()
def color_block(p_todo, p_safe=True):
color_code = progress_color_code(p_todo, p_safe)
ansi_code = int_to_ansi(color_code, p_safe=p_safe, p_background=color_code)
return '{} {}'.format(ansi_code, NEUTRAL_COLOR)
......@@ -20,6 +20,45 @@ from topydo.lib.Config import config
NEUTRAL_COLOR = '\033[0m'
def int_to_ansi(p_int, p_decorator='normal', p_safe=True, p_background=''):
"""
Returns ansi code for color based on xterm color id (0-255) and
decoration, where decoration can be one of: normal, bold, faint,
italic, or underline. When p_safe is True, resulting ansi code is
constructed in most compatible way, but with support for only base 16
colors.
"""
decoration_dict = {
'normal': '0',
'bold': '1',
'faint': '2',
'italic': '3',
'underline': '4'
}
decoration = decoration_dict[p_decorator]
try:
if p_safe:
if p_background:
p_background = ';4{}'.format(p_background)
if 8 > int(p_int) >= 0:
return '\033[{};3{}{}m'.format(decoration, str(p_int), p_background)
elif 16 > int(p_int):
p_int = int(p_int) - 8
return '\033[{};1;3{}{}m'.format(decoration, str(p_int), p_background)
if 256 > int(p_int) >= 0:
if p_background:
p_background = ';48;5;{}'.format(str(p_int))
return '\033[{};38;5;{}{}m'.format(decoration, str(p_int), p_background)
else:
return NEUTRAL_COLOR
except ValueError:
return None
class Colors(object):
def __init__(self):
......@@ -29,39 +68,6 @@ class Colors(object):
self.metadata_color = config().metadata_color()
self.link_color = config().link_color()
def _int_to_ansi(self, p_int, p_decorator='normal', p_safe=True):
"""
Returns ansi code for color based on xterm color id (0-255) and
decoration, where decoration can be one of: normal, bold, faint,
italic, or underline. When p_safe is True, resulting ansi code is
constructed in most compatible way, but with support for only base 16
colors.
"""
decoration_dict = {
'normal': '0',
'bold': '1',
'faint': '2',
'italic': '3',
'underline': '4'
}
decoration = decoration_dict[p_decorator]
try:
if p_safe:
if 8 > int(p_int) >= 0:
return '\033[{};3{}m'.format(decoration, str(p_int))
elif 16 > int(p_int):
p_int = int(p_int) - 8
return '\033[{};1;3{}m'.format(decoration, str(p_int))
if 256 > int(p_int) >= 0:
return '\033[{};38;5;{}m'.format(decoration, str(p_int))
else:
return NEUTRAL_COLOR
except ValueError:
return None
def _name_to_int(self, p_color_name):
""" Returns xterm color id from color name. """
color_names_dict = {
......@@ -92,14 +98,14 @@ class Colors(object):
""" Returns ansi color code from color name. """
number = self._name_to_int(p_color_name)
return self._int_to_ansi(number, p_decorator)
return int_to_ansi(number, p_decorator)
def _get_ansi(self, p_color, p_decorator):
""" Returns ansi color code from color name or xterm color id. """
if p_color == '':
ansi = ''
else:
ansi = self._int_to_ansi(p_color, p_decorator, False)
ansi = int_to_ansi(p_color, p_decorator, False)
if not ansi:
ansi = self._name_to_ansi(p_color, p_decorator)
......
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