Commit b67244fe authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge pull request #27 from mruwek/colorscheme

Add possibility to configure colorscheme of topydo
parents 82144a4b 17c29f55
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 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/>.
""" Tests for the colorscheme functionality. """
import unittest
from topydo.lib.Colors import Colors, NEUTRAL_COLOR
from topydo.lib.Config import config
from test.TopydoTest import TopydoTest
class ColorsTest(TopydoTest):
def test_project_color1(self):
config(p_overrides={('colorscheme', 'project_color'): '2'})
color = Colors().get_project_color()
self.assertEqual(color, '\033[1;38;5;2m')
def test_project_color2(self):
config(p_overrides={('colorscheme', 'project_color'): 'Foo'})
color = Colors().get_project_color()
self.assertEqual(color, NEUTRAL_COLOR)
def test_project_color3(self):
config(p_overrides={('colorscheme', 'project_color'): 'yellow'})
color = Colors().get_project_color()
self.assertEqual(color, '\033[1;38;5;3m')
def test_project_color4(self):
config(p_overrides={('colorscheme', 'project_color'): '686'})
color = Colors().get_project_color()
self.assertEqual(color, NEUTRAL_COLOR)
def test_context_color1(self):
config(p_overrides={('colorscheme', 'context_color'): '35'})
color = Colors().get_context_color()
self.assertEqual(color, '\033[1;38;5;35m')
def test_context_color2(self):
config(p_overrides={('colorscheme', 'context_color'): 'Bar'})
color = Colors().get_context_color()
self.assertEqual(color, NEUTRAL_COLOR)
def test_context_color3(self):
config(p_overrides={('colorscheme', 'context_color'): 'magenta'})
color = Colors().get_context_color()
self.assertEqual(color, '\033[1;38;5;5m')
def test_context_color4(self):
config(p_overrides={('colorscheme', 'context_color'): '392'})
color = Colors().get_context_color()
self.assertEqual(color, NEUTRAL_COLOR)
def test_metadata_color1(self):
config(p_overrides={('colorscheme', 'metadata_color'): '128'})
color = Colors().get_metadata_color()
self.assertEqual(color, '\033[1;38;5;128m')
def test_metadata_color2(self):
config(p_overrides={('colorscheme', 'metadata_color'): 'Baz'})
color = Colors().get_metadata_color()
self.assertEqual(color, NEUTRAL_COLOR)
def test_metadata_color3(self):
config(p_overrides={('colorscheme', 'metadata_color'): 'light-red'})
color = Colors().get_metadata_color()
self.assertEqual(color, '\033[1;38;5;9m')
def test_metadata_color4(self):
config(p_overrides={('colorscheme', 'metadata_color'): '777'})
color = Colors().get_metadata_color()
self.assertEqual(color, NEUTRAL_COLOR)
def test_link_color1(self):
config(p_overrides={('colorscheme', 'link_color'): '77'})
color = Colors().get_link_color()
self.assertEqual(color, '\033[4;38;5;77m')
def test_link_color2(self):
config(p_overrides={('colorscheme', 'link_color'): 'FooBar'})
color = Colors().get_link_color()
self.assertEqual(color, NEUTRAL_COLOR)
def test_link_color3(self):
config(p_overrides={('colorscheme', 'link_color'): 'red'})
color = Colors().get_link_color()
self.assertEqual(color, '\033[4;38;5;1m')
def test_link_color4(self):
config(p_overrides={('colorscheme', 'link_color'): '777'})
color = Colors().get_link_color()
self.assertEqual(color, NEUTRAL_COLOR)
def test_priority_color1(self):
config("test/data/ColorsTest1.conf")
color = Colors().get_priority_colors()
self.assertEqual(color['A'], '\033[0;38;5;1m')
self.assertEqual(color['B'], '\033[0;38;5;2m')
self.assertEqual(color['C'], '\033[0;38;5;3m')
def test_priority_color2(self):
config("test/data/ColorsTest2.conf")
color = Colors().get_priority_colors()
self.assertEqual(color['A'], '\033[0;38;5;5m')
self.assertEqual(color['B'], '\033[0;38;5;6m')
self.assertEqual(color['C'], '\033[0;38;5;7m')
def test_priority_color3(self):
config("test/data/ColorsTest3.conf")
color = Colors().get_priority_colors()
self.assertEqual(color['Z'], NEUTRAL_COLOR)
self.assertEqual(color['D'], '\033[0;38;5;1m')
self.assertEqual(color['C'], '\033[0;38;5;7m')
def test_priority_color4(self):
config("test/data/ColorsTest4.conf")
color = Colors().get_priority_colors()
self.assertEqual(color['A'], NEUTRAL_COLOR)
self.assertEqual(color['B'], NEUTRAL_COLOR)
self.assertEqual(color['C'], NEUTRAL_COLOR)
def test_empty_color_values(self):
config("test/data/ColorsTest5.conf")
pri_color = Colors().get_priority_colors()
project_color = Colors().get_project_color()
context_color = Colors().get_context_color()
link_color = Colors().get_link_color()
metadata_color = Colors().get_metadata_color()
self.assertEqual(pri_color['A'], NEUTRAL_COLOR)
self.assertEqual(pri_color['B'], NEUTRAL_COLOR)
self.assertEqual(pri_color['C'], NEUTRAL_COLOR)
self.assertEqual(project_color, '')
self.assertEqual(context_color, '')
self.assertEqual(link_color, '')
self.assertEqual(metadata_color, '')
def test_empty_colorscheme(self):
config("test/data/config1")
pri_color = Colors().get_priority_colors()
project_color = Colors().get_project_color()
context_color = Colors().get_context_color()
link_color = Colors().get_link_color()
metadata_color = Colors().get_metadata_color()
self.assertEqual(pri_color['A'], '\033[0;38;5;6m')
self.assertEqual(pri_color['B'], '\033[0;38;5;3m')
self.assertEqual(pri_color['C'], '\033[0;38;5;4m')
self.assertEqual(project_color, '\033[1;38;5;1m')
self.assertEqual(context_color, '\033[1;38;5;5m')
self.assertEqual(link_color, '\033[4;38;5;6m')
self.assertEqual(metadata_color, '\033[1;38;5;2m')
[colorscheme]
priority_colors = A:1,B:2,C:3
[colorscheme]
priority_colors = A:magenta,B:cyan,C:gray
[colorscheme]
priority_colors = A:magenta,B:cyan,C:7,D:red,Z:foobar
[colorscheme]
priority_colors = A:,B:,C:
[colorscheme]
priority_colors =
project_color =
context_color =
link_color =
metadata_color =
...@@ -30,3 +30,16 @@ ignore_weekends = 1 ...@@ -30,3 +30,16 @@ ignore_weekends = 1
append_parent_projects = 0 append_parent_projects = 0
; Add parent contexts when adding sub todo items ; Add parent contexts when adding sub todo items
append_parent_contexts = 0 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: ...@@ -38,7 +38,7 @@ class _Config:
(such as todo.txt location passed with -t). The key is a tuple of (such as todo.txt location passed with -t). The key is a tuple of
(section, option), the value is the option's value. (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 = { self.defaults = {
# topydo # topydo
...@@ -67,6 +67,13 @@ class _Config: ...@@ -67,6 +67,13 @@ class _Config:
# dep # dep
'append_parent_projects': '0', 'append_parent_projects': '0',
'append_parent_contexts': '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 = {} self.config = {}
...@@ -187,6 +194,52 @@ class _Config: ...@@ -187,6 +194,52 @@ class _Config:
hidden_tags = self.cp.get('ls', 'hide_tags') hidden_tags = self.cp.get('ls', 'hide_tags')
return [] if hidden_tags == '' else hidden_tags.split(',') 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): def config(p_path=None, p_overrides=None):
""" """
Retrieve the config instance. Retrieve the config instance.
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
import re import re
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Colors import Colors, NEUTRAL_COLOR
class PrettyPrinterFilter(object): class PrettyPrinterFilter(object):
""" """
...@@ -31,18 +32,6 @@ class PrettyPrinterFilter(object): ...@@ -31,18 +32,6 @@ class PrettyPrinterFilter(object):
""" Default implementation returns an unmodified todo string. """ """ Default implementation returns an unmodified todo string. """
return p_todo_str 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): class PrettyPrinterColorFilter(PrettyPrinterFilter):
""" """
Adds colors to the todo string by inserting ANSI codes. Adds colors to the todo string by inserting ANSI codes.
...@@ -53,10 +42,17 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter): ...@@ -53,10 +42,17 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
def filter(self, p_todo_str, p_todo): def filter(self, p_todo_str, p_todo):
""" Applies the colors. """ """ 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(): if config().colors():
color = NEUTRAL_COLOR color = NEUTRAL_COLOR
try: try:
color = PRIORITY_COLORS[p_todo.priority()] color = priority_colors[p_todo.priority()]
except KeyError: except KeyError:
pass pass
...@@ -65,12 +61,12 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter): ...@@ -65,12 +61,12 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
p_todo_str = re.sub( p_todo_str = re.sub(
r'\B(\+|@)(\S*\w)', r'\B(\+|@)(\S*\w)',
lambda m: ( lambda m: (
CONTEXT_COLOR if m.group(0)[0] == "+" context_color if m.group(0)[0] == "@"
else PROJECT_COLOR)+m.group(0)+color, else project_color)+m.group(0)+color,
p_todo_str) p_todo_str)
p_todo_str = re.sub(r'\b\S+:[^/\s]\S+\b',METADATA_COLOR+r'\g<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. # 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'(^|\s)(\w+:){1}(//\S+)',' '+link_color+r'\2\3'+color,p_todo_str)
p_todo_str += NEUTRAL_COLOR p_todo_str += NEUTRAL_COLOR
# chnge any group of reduntant multiple NEUTRAL_GROUP occurrence to only one # 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) 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