Commit 44025480 authored by Jacek Sowiński's avatar Jacek Sowiński

Add possibility to configure colorscheme of topydo

- Add new module `topydo.lib.Colors` and class `Colors` for handling
  output colors and their ANSI codes.
- Create new config options with colors for priorities, projects,
  contexts, metadata, and links and place them under "colorscheme"
  section. When certain config option is absent default color code will be
  applied.
- Empty values:
    ! priority_colors will fall back to NEUTRAL_COLOR
    ! project_color, context_color, link_color, metadata_color will
      return empty string, so priority_color (if configured and applicable) of
      specific item should be used
- Return NEUTRAL_COLOR if supplied color number is out range
- Support user-friendly color names in config. At least for 16 base
  terminal colors.
parent 5bcf70e6
......@@ -30,3 +30,16 @@ ignore_weekends = 1
append_parent_projects = 0
; Add parent contexts when adding sub todo items
append_parent_contexts = 0
[colorscheme]
; Configure colorscheme. Accepted values are: black, [light-]red, [light-]green,
; [light-]yellow, [light-]blue, [light-]magenta, [light-]cyan, white
; [light-]gray, darkgray or numbers from 0 to 255. When number is specified color
; is matched from Xterm color chart available here:
; http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
; priority_colors = A:cyan,B:yellow,C:blue
; project_color = red
; context_color = magenta
; metadata_color = green
; link_color = light-cyan
# 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/>.
""" This module serves for managing output colors. """
from topydo.lib.Config import config
NEUTRAL_COLOR = '\033[0m'
class Colors(object):
def __init__(self):
self.priority_colors = config().priority_colors()
self.project_color = config().project_color()
self.context_color = config().context_color()
self.metadata_color = config().metadata_color()
self.link_color = config().link_color()
def _int_to_ansi(self, p_int, p_decorator='normal'):
"""
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.
"""
decoration_dict = {
'normal': '0',
'bold': '1',
'faint': '2',
'italic': '3',
'underline': '4'
}
decoration = decoration_dict[p_decorator]
try:
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 = {
'black': 0,
'red': 1,
'green': 2,
'yellow': 3,
'blue': 4,
'magenta': 5,
'cyan': 6,
'gray': 7,
'darkgray': 8,
'light-red': 9,
'light-green': 10,
'light-yellow': 11,
'light-blue': 12,
'light-magenta': 13,
'light-cyan': 14,
'white': 15,
}
try:
return color_names_dict[p_color_name]
except KeyError:
return 404
def _name_to_ansi(self, p_color_name, p_decorator):
""" Returns ansi color code from color name. """
number = self._name_to_int(p_color_name)
return self._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)
if not ansi:
ansi = self._name_to_ansi(p_color, p_decorator)
return ansi
def get_priority_colors(self):
pri_ansi_colors = dict()
for pri in self.priority_colors:
color = self._get_ansi(self.priority_colors[pri], 'normal')
if color == '':
color = NEUTRAL_COLOR
pri_ansi_colors[pri] = color
return pri_ansi_colors
def get_project_color(self):
return self._get_ansi(self.project_color, 'bold')
def get_context_color(self):
return self._get_ansi(self.context_color, 'bold')
def get_metadata_color(self):
return self._get_ansi(self.metadata_color, 'bold')
def get_link_color(self):
return self._get_ansi(self.link_color, 'underline')
......@@ -38,7 +38,7 @@ class _Config:
(such as todo.txt location passed with -t). The key is a tuple of
(section, option), the value is the option's value.
"""
self.sections = ['topydo', 'tags', 'sort', 'ls', 'dep']
self.sections = ['topydo', 'tags', 'sort', 'ls', 'dep', 'colorscheme']
self.defaults = {
# topydo
......@@ -67,6 +67,13 @@ class _Config:
# dep
'append_parent_projects': '0',
'append_parent_contexts': '0',
# colorscheme
'project_color': '1',
'context_color': '5',
'metadata_color': '2',
'link_color': '6',
'priority_colors': 'A:6,B:3,C:4',
}
self.config = {}
......@@ -186,6 +193,52 @@ class _Config:
hidden_tags = self.cp.get('ls', 'hide_tags')
return [] if hidden_tags == '' else hidden_tags.split(',')
def priority_colors(self):
""" Returns a dict with priorities as keys and color numbers as value. """
pri_colors_str = self.cp.get('colorscheme', 'priority_colors')
def _str_to_dict(p_string):
pri_colors_dict = dict()
for pri_color in p_string.split(','):
pri, color = pri_color.split(':')
pri_colors_dict[pri] = color
return pri_colors_dict
try:
if pri_colors_str == '':
pri_colors_dict = {'A':'', 'B': '', 'C': ''}
else:
pri_colors_dict = _str_to_dict(pri_colors_str)
except ValueError:
pri_colors_dict = _str_to_dict(self.defaults['priority_colors'])
return pri_colors_dict
def project_color(self):
try:
return self.cp.get('colorscheme', 'project_color')
except ValueError:
return int(self.defaults['project_color'])
def context_color(self):
try:
return self.cp.get('colorscheme', 'context_color')
except ValueError:
return int(self.defaults['context_color'])
def metadata_color(self):
try:
return self.cp.get('colorscheme', 'metadata_color')
except ValueError:
return int(self.defaults['metadata_color'])
def link_color(self):
try:
return self.cp.get('colorscheme', 'link_color')
except ValueError:
return int(self.defaults['link_color'])
def config(p_path=None, p_overrides=None):
"""
Retrieve the config instance.
......
......@@ -19,6 +19,7 @@
import re
from topydo.lib.Config import config
from topydo.lib.Colors import Colors, NEUTRAL_COLOR
class PrettyPrinterFilter(object):
"""
......@@ -31,18 +32,6 @@ class PrettyPrinterFilter(object):
""" Default implementation returns an unmodified todo string. """
return p_todo_str
PRIORITY_COLORS = {
'A': '\033[0;36m', # cyan
'B': '\033[0;33m', # yellow
'C': '\033[0;34m' # blue
}
PROJECT_COLOR = '\033[1;31m' # color for + keyword : red
CONTEXT_COLOR = '\033[1;35m' # color for @ keyword : magenta
METADATA_COLOR = '\033[1;32m' # color for foo:bar metadata : green
LINK_COLOR = '\033[4;36m' # color for foo://bar.baz links : cyan/underline
NEUTRAL_COLOR = '\033[0m'
class PrettyPrinterColorFilter(PrettyPrinterFilter):
"""
Adds colors to the todo string by inserting ANSI codes.
......@@ -53,10 +42,17 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
def filter(self, p_todo_str, p_todo):
""" Applies the colors. """
colorscheme = Colors()
priority_colors = colorscheme.get_priority_colors()
project_color = colorscheme.get_project_color()
context_color = colorscheme.get_context_color()
metadata_color = colorscheme.get_metadata_color()
link_color = colorscheme.get_link_color()
if config().colors():
color = NEUTRAL_COLOR
try:
color = PRIORITY_COLORS[p_todo.priority()]
color = priority_colors[p_todo.priority()]
except KeyError:
pass
......@@ -65,12 +61,12 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
p_todo_str = re.sub(
r'\B(\+|@)(\S*\w)',
lambda m: (
CONTEXT_COLOR if m.group(0)[0] == "+"
else PROJECT_COLOR)+m.group(0)+color,
context_color if m.group(0)[0] == "+"
else project_color)+m.group(0)+color,
p_todo_str)
p_todo_str = re.sub(r'\b\S+:[^/\s]\S+\b',METADATA_COLOR+r'\g<0>'+color,p_todo_str)
# add LINK_COLOR to any valid URL specified outside of the tag.
p_todo_str = re.sub(r'(^|\s)(\w+:){1}(//\S+)',' '+LINK_COLOR+r'\2\3'+color,p_todo_str)
p_todo_str = re.sub(r'\b\S+:[^/\s]\S+\b',metadata_color+r'\g<0>'+color,p_todo_str)
# add link_color to any valid URL specified outside of the tag.
p_todo_str = re.sub(r'(^|\s)(\w+:){1}(//\S+)',' '+link_color+r'\2\3'+color,p_todo_str)
p_todo_str += NEUTRAL_COLOR
# chnge any group of reduntant multiple NEUTRAL_GROUP occurrence to only one
p_todo_str = re.sub(r'('+re.escape(NEUTRAL_COLOR)+')+',NEUTRAL_COLOR,p_todo_str)
......
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