diff options
Diffstat (limited to 'deluge/ui/console/utils/colors.py')
-rw-r--r-- | deluge/ui/console/utils/colors.py | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/deluge/ui/console/utils/colors.py b/deluge/ui/console/utils/colors.py new file mode 100644 index 0000000..cc414fe --- /dev/null +++ b/deluge/ui/console/utils/colors.py @@ -0,0 +1,323 @@ +# +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +import re + +from deluge.ui.console.utils import format_utils + +try: + import curses +except ImportError: + pass + +log = logging.getLogger(__name__) + +colors = [ + 'COLOR_BLACK', + 'COLOR_BLUE', + 'COLOR_CYAN', + 'COLOR_GREEN', + 'COLOR_MAGENTA', + 'COLOR_RED', + 'COLOR_WHITE', + 'COLOR_YELLOW', +] + +# {(fg, bg): pair_number, ...} +color_pairs = {('white', 'black'): 0} # Special case, can't be changed + +# Some default color schemes +schemes = { + 'input': ('white', 'black'), + 'normal': ('white', 'black'), + 'status': ('yellow', 'blue', 'bold'), + 'info': ('white', 'black', 'bold'), + 'error': ('red', 'black', 'bold'), + 'success': ('green', 'black', 'bold'), + 'event': ('magenta', 'black', 'bold'), + 'selected': ('black', 'white', 'bold'), + 'marked': ('white', 'blue', 'bold'), + 'selectedmarked': ('blue', 'white', 'bold'), + 'header': ('green', 'black', 'bold'), + 'filterstatus': ('green', 'blue', 'bold'), +} + +# Colors for various torrent states +state_color = { + 'Seeding': '{!blue,black,bold!}', + 'Downloading': '{!green,black,bold!}', + 'Paused': '{!white,black!}', + 'Checking': '{!green,black!}', + 'Queued': '{!yellow,black!}', + 'Error': '{!red,black,bold!}', + 'Moving': '{!green,black,bold!}', +} + +type_color = { + bool: '{!yellow,black,bold!}', + int: '{!green,black,bold!}', + float: '{!green,black,bold!}', + str: '{!cyan,black,bold!}', + list: '{!magenta,black,bold!}', + dict: '{!white,black,bold!}', +} + +tab_char = '\t' +color_tag_start = '{!' +color_tag_end = '!}' + + +def get_color_pair(fg, bg): + return color_pairs[(fg, bg)] + + +def init_colors(): + curses.start_color() + + # We want to redefine white/black as it makes underlining work for some terminals + # but can also fail on others, so we try/except + + def define_pair(counter, fg_name, bg_name, fg, bg): + try: + curses.init_pair(counter, fg, bg) + color_pairs[(fg_name, bg_name)] = counter + counter += 1 + except (curses.error, ValueError) as ex: + log.debug(f'Color pair {fg_name} {bg_name} not available: {ex}') + return counter + + # Create the color_pairs dict + counter = 1 + for fg in colors: + for bg in colors: + counter = define_pair( + counter, + fg[6:].lower(), + bg[6:].lower(), + getattr(curses, fg), + getattr(curses, bg), + ) + + counter = define_pair(counter, 'white', 'grey', curses.COLOR_WHITE, 241) + counter = define_pair(counter, 'black', 'whitegrey', curses.COLOR_BLACK, 249) + counter = define_pair(counter, 'magentadark', 'white', 99, curses.COLOR_WHITE) + + +class BadColorString(Exception): + pass + + +def check_tag_count(string): + """Raise BadColorString if color tag open/close not equal.""" + if string.count(color_tag_start) != string.count(color_tag_end): + raise BadColorString('Number of {! is not equal to number of !}') + + +def replace_tabs(line): + """ + Returns a string with tabs replaced with spaces. + + """ + for i in range(line.count(tab_char)): + tab_length = 8 - (len(line[: line.find(tab_char)]) % 8) + line = line.replace(tab_char, b' ' * tab_length, 1) + return line + + +def strip_colors(line): + """ + Returns a string with the color formatting removed. + + """ + check_tag_count(line) + + # Remove all the color tags + while line.find(color_tag_start) != -1: + tag_start = line.find(color_tag_start) + tag_end = line.find(color_tag_end) + 2 + line = line[:tag_start] + line[tag_end:] + + return line + + +def get_line_length(line): + """ + Returns the string length without the color formatting. + + """ + # Remove all the color tags + line = strip_colors(line) + + # Replace tabs with the appropriate amount of spaces + line = replace_tabs(line) + return len(line) + + +def get_line_width(line): + """ + Get width of string considering double width characters + + """ + # Remove all the color tags + line = strip_colors(line) + + # Replace tabs with the appropriate amount of spaces + line = replace_tabs(line) + return format_utils.strwidth(line) + + +def parse_color_string(string): + """Parses a string and returns a list of 2-tuples (color, string). + + Args: + string (str): The string to parse. + """ + check_tag_count(string) + + ret = [] + last_color_attr = None + # Keep track of where the strings + while string.find(color_tag_start) != -1: + begin = string.find(color_tag_start) + if begin > 0: + ret.append( + ( + curses.color_pair( + color_pairs[(schemes['input'][0], schemes['input'][1])] + ), + string[:begin], + ) + ) + + end = string.find(color_tag_end) + if end == -1: + raise BadColorString('Missing closing "!}"') + + # Get a list of attributes in the bracketed section + attrs = string[begin + 2 : end].split(',') + + if len(attrs) == 1 and not attrs[0].strip(' '): + raise BadColorString('No description in {! !}') + + def apply_attrs(cp, attrs): + # This function applies any additional attributes as necessary + for attr in attrs: + if attr == 'ignore': + continue + mode = '+' + if attr[0] in ['+', '-']: + mode = attr[0] + attr = attr[1:] + if mode == '+': + cp |= getattr(curses, 'A_' + attr.upper()) + else: + cp ^= getattr(curses, 'A_' + attr.upper()) + return cp + + # Check for a builtin type first + if attrs[0] in schemes: + pair = (schemes[attrs[0]][0], schemes[attrs[0]][1]) + if pair not in color_pairs: + log.debug('Color pair does not exist: %s, attrs: %s', pair, attrs) + pair = ('white', 'black') + # Get the color pair number + color_pair = curses.color_pair(color_pairs[pair]) + color_pair = apply_attrs(color_pair, schemes[attrs[0]][2:]) + last_color_attr = color_pair + else: + attrlist = ['blink', 'bold', 'dim', 'reverse', 'standout', 'underline'] + + if attrs[0][0] in ['+', '-']: + # Color is not given, so use last color + if last_color_attr is None: + raise BadColorString( + 'No color value given when no previous color was used!: %s' + % (attrs[0]) + ) + color_pair = last_color_attr + for i, attr in enumerate(attrs): + if attr[1:] not in attrlist: + raise BadColorString('Bad attribute value!: %s' % (attr)) + else: + # This is a custom color scheme + fg = attrs[0] + bg = 'black' # Default to 'black' if no bg is chosen + if len(attrs) > 1: + bg = attrs[1] + try: + pair = (fg, bg) + if pair not in color_pairs: + # Color pair missing, this could be because the + # terminal settings allows no colors. If background is white, we + # assume this means selection, and use "white", "black" + reverse + # To have white background and black foreground + log.debug('Color pair does not exist: %s', pair) + if pair[1] == 'white': + if attrs[2] == 'ignore': + attrs[2] = 'reverse' + else: + attrs.append('reverse') + pair = ('white', 'black') + color_pair = curses.color_pair(color_pairs[pair]) + last_color_attr = color_pair + attrs = attrs[2:] # Remove colors + except KeyError: + raise BadColorString(f'Bad color value in tag: {fg},{bg}') + # Check for additional attributes and OR them to the color_pair + color_pair = apply_attrs(color_pair, attrs) + last_color_attr = color_pair + # We need to find the text now, so lets try to find another {! and if + # there isn't one, then it's the rest of the string + next_begin = string.find(color_tag_start, end) + + if next_begin == -1: + ret.append((color_pair, replace_tabs(string[end + 2 :]))) + break + else: + ret.append((color_pair, replace_tabs(string[end + 2 : next_begin]))) + string = string[next_begin:] + + if not ret: + # There was no color scheme so we add it with a 0 for white on black + ret = [(0, string)] + return ret + + +class ConsoleColorFormatter: + """ + Format help in a way suited to deluge CmdLine mode - colors, format, indentation... + """ + + replace_dict = { + '<torrent-id>': '{!green!}%s{!input!}', + '<torrent>': '{!green!}%s{!input!}', + '<command>': '{!green!}%s{!input!}', + '<state>': '{!yellow!}%s{!input!}', + '\\.\\.\\.': '{!yellow!}%s{!input!}', + '\\s\\*\\s': '{!blue!}%s{!input!}', + '(?<![\\-a-z])(-[a-zA-Z0-9])': '{!red!}%s{!input!}', + # "(\-[a-zA-Z0-9])": "{!red!}%s{!input!}", + '--[_\\-a-zA-Z0-9]+': '{!green!}%s{!input!}', + '(\\[|\\])': '{!info!}%s{!input!}', + '<tab>': '{!white!}%s{!input!}', + '[_A-Z]{3,}': '{!cyan!}%s{!input!}', + '<key>': '{!cyan!}%s{!input!}', + '<value>': '{!cyan!}%s{!input!}', + 'usage:': '{!info!}%s{!input!}', + '<download-folder>': '{!yellow!}%s{!input!}', + '<torrent-file>': '{!green!}%s{!input!}', + } + + def format_colors(self, string): + def r(repl): + return lambda s: repl % s.group() + + for key, replacement in self.replace_dict.items(): + string = re.sub(key, r(replacement), string) + return string |