From f2ce9b3679d996202c6c1845ce95522e1e61a1c6 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 03:41:26 +0000 Subject: [PATCH 01/14] feature/color_manager: add color manager --- suplemon/color_pairs.py | 153 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 suplemon/color_pairs.py diff --git a/suplemon/color_pairs.py b/suplemon/color_pairs.py new file mode 100644 index 0000000..68d6a5c --- /dev/null +++ b/suplemon/color_pairs.py @@ -0,0 +1,153 @@ +# -*- encoding: utf-8 +""" +Manage curses color pairs +""" + +import curses +import logging + + +class ColorPairs: + def __init__(self): + self.logger = logging.getLogger(__name__ + "." + ColorPairs.__name__) + self._colors = dict() + + # color_pair(0) is hardcoded + # https://docs.python.org/3/library/curses.html#curses.init_pair + self._color_count = 1 + + # dynamic in case terminal does not support use_default_colors() + self._invalid = curses.COLOR_RED + self._default_fg = -1 + self._default_bg = -1 + + def set_default_fg(self, color): + self._default_fg = color + + def set_default_bg(self, color): + self._default_bg = color + + def _get(self, name, index=None, default=None, log_missing=True): + ret = self._colors.get(str(name), None) + if ret is None: + if log_missing: + self.logger.warning("Color '%s' not initialized. Maybe some issue with your theme?" % name) + return default + if index is not None: + return ret[index] + return ret + + # FIXME: evaluate default returns on error. none? exception? ._default_[fb]g? .invalid? hardcoded? pair(0)? + def get(self, name): + """ Return colorpair ORed attribs or a fallback """ + return self._get(name, index=1, default=curses.color_pair(0)) + + def get_alt(self, name, alt): + """ Return colorpair ORed attribs or alt """ + return self._get(name, index=1, default=alt, log_missing=False) + + def get_fg(self, name): + """ Return foreground color as integer """ + return self._get(name, index=2, default=curses.COLOR_WHITE) + + def get_bg(self, name): + """ Return background color as integer """ + return self._get(name, index=3, default=curses.COLOR_RED) + + def get_color(self, name): + """ Alternative for get(name) """ + return self.get(name) + + def get_all(self, name): + """ color, fg, bg, attrs = get_all("something") """ + ret = self._get(name) + if ret is None: + return (None, None, None, None) + return ret[1:] + + def contains(self, name): + return str(name) in self._colors + + def add_translate(self, name, fg, bg, attributes=None): + """ + Store or update color definition. + fg and bg can be of form "blue" or "color162". + attributes can be a list of attribute names like "bold" or "underline". + """ + return self.add_curses( + name, + self.translate_color(fg, check_for="fg"), + self.translate_color(bg, check_for="bg"), + self.translate_attributes(attributes) + ) + + def add_curses(self, name, fg, bg, attrs=0): + """ Store or update color definition. fg, bg and attrs must be valid curses values """ + # FIXME: catch invalid colors, attrs,... + name = str(name) + if name in self._colors: + # Redefine exiting color pair + index, color, fg, bg, attrs = self._colors[name] + self.logger.info("Updating exiting color pair with index %i and name '%s'" % (index, name)) + else: + # Create new color pair + index = self._color_count + self.logger.info("Creating new color pair with index %i and name '%s'" % (index, name)) + if index < curses.COLOR_PAIRS: + self._color_count += 1 + else: + self.logger.warning( + "Failed to create new color pair for " + + "'%s', the terminal description for '%s' only supports up to %i color pairs" % + (name, curses.termname().decode("utf-8"), curses.COLOR_PAIRS) + ) + color = curses.color_pair(0) | attrs + self._colors[name] = (0, color, curses.COLOR_WHITE, curses.COLOR_BLACK, attrs) + return color + curses.init_pair(index, fg, bg) + color = curses.color_pair(index) | attrs + self._colors[name] = (index, color, fg, bg, attrs) + return color + + def translate_attributes(self, attributes): + if attributes is None: + return 0 + val = 0 + for attrib in attributes: + val |= getattr(curses, "A_" + attrib.upper(), 0) + return val + + def translate_color(self, color, check_for=None): + if color is None: + return self._invalid + + color_i = getattr(curses, "COLOR_" + color.upper(), None) + if color_i is not None: + return color_i + + color = color.lower() + if color == "default": + # FIXME: what to return if check_for is not set? + return self._default_fg if check_for == "fg" else self._default_bg + elif color.startswith("color"): + color_i = color[len("color"):] + elif color.startswith("colour"): + color_i = color[len("colour"):] + else: + self.logger.warning("Invalid color specified: '%s'" % color) + return self._invalid + + try: + color_i = int(color_i) + except: + self.logger.warning("Invalid color specified: '%s'" % color) + return self._invalid + + if color_i >= curses.COLORS: + self.logger.warning( + "The terminal description for '%s' does not support more than %i colors. Specified color was %s" % + (curses.termname().decode("utf-8"), curses.COLORS, color) + ) + return self._invalid + + return color_i From 14658882b178251d8884f5470dc9f1e9030fa499 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 03:41:57 +0000 Subject: [PATCH 02/14] feature/color_manager: use color manager --- suplemon/config/defaults.json | 35 +++++++++- suplemon/line.py | 10 +-- suplemon/modules/linter.py | 4 +- suplemon/ui.py | 119 ++++++++++++++-------------------- suplemon/viewer.py | 40 ++++++++---- 5 files changed, 113 insertions(+), 95 deletions(-) diff --git a/suplemon/config/defaults.json b/suplemon/config/defaults.json index bcc057f..c7b77d5 100644 --- a/suplemon/config/defaults.json +++ b/suplemon/config/defaults.json @@ -100,7 +100,9 @@ "\uFEFF": "\u2420" }, // Whether to visually show white space chars - "show_white_space": true, + "show_white_space": false, + // Whether to ignore theme whitespace color + "ignore_theme_whitespace": false, // Show tab indicators in whitespace "show_tab_indicators": true, // Tab indicator charatrer @@ -139,7 +141,34 @@ "show_legend": true, // Show the bottom status bar "show_bottom_bar": true, - // Invert status bar colors (switch text and background colors) - "invert_status_bars": false + // Theme for 8 colors + "colors_8": { + // Another variant for linenumbers (and maybe status_*) is black, black, bold + "status_top": { "fg": "white", "bg": "black" }, + "status_bottom": { "fg": "white", "bg": "black" }, + "linenumbers": { "fg": "white", "bg": "black" }, + "linenumbers_lint_error": { "fg": "red", "bg": "black" }, + "editor": { "fg": "default", "bg": "default" }, + "editor_whitespace": { "fg": "black", "bg": "default", "attribs": [ "bold" ] } + }, + // Theme for 88 colors + "colors_88": { + // Copy of colors_8; this needs an own default theme + "status_top": { "fg": "white", "bg": "black" }, + "status_bottom": { "fg": "white", "bg": "black" }, + "linenumbers": { "fg": "white", "bg": "black" }, + "linenumbers_lint_error": { "fg": "red", "bg": "black" }, + "editor": { "fg": "default", "bg": "default" }, + "editor_whitespace": { "fg": "black", "bg": "default", "attribs": [ "bold" ] } + }, + // Theme for 256 colors + "colors_256": { + "status_top": { "fg": "color250", "bg": "black" }, + "status_bottom": { "fg": "color250", "bg": "black" }, + "linenumbers": { "fg": "color240", "bg": "black" }, + "linenumbers_lint_error": { "fg": "color204", "bg": "black" }, + "editor": { "fg": "default", "bg": "default" }, + "editor_whitespace": { "fg": "color240", "bg": "default" } + } } } diff --git a/suplemon/line.py b/suplemon/line.py index 3cea9b0..bec422c 100644 --- a/suplemon/line.py +++ b/suplemon/line.py @@ -10,7 +10,7 @@ def __init__(self, data=""): data = data.data self.data = data self.x_scroll = 0 - self.number_color = 8 + self.state = None def __getitem__(self, i): return self.data[i] @@ -38,8 +38,8 @@ def set_data(self, data): data = data.get_data() self.data = data - def set_number_color(self, color): - self.number_color = color + def set_state(self, state): + self.state = state def find(self, what, start=0): return self.data.find(what, start) @@ -47,5 +47,5 @@ def find(self, what, start=0): def strip(self, *args): return self.data.strip(*args) - def reset_number_color(self): - self.number_color = 8 + def reset_state(self): + self.state = None diff --git a/suplemon/modules/linter.py b/suplemon/modules/linter.py index 5997f7c..579fefa 100644 --- a/suplemon/modules/linter.py +++ b/suplemon/modules/linter.py @@ -90,10 +90,10 @@ def lint_file(self, file): line = editor.lines[line_no] if line_no+1 in linting.keys(): line.linting = linting[line_no+1] - line.set_number_color(1) + line.set_state("lint_error") else: line.linting = False - line.reset_number_color() + line.reset_state() def get_msgs_on_line(self, editor, line_no): line = editor.lines[line_no] diff --git a/suplemon/ui.py b/suplemon/ui.py index 6fbfbcf..00e1c6c 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -10,6 +10,7 @@ from .prompt import Prompt, PromptBool, PromptFiltered, PromptFile, PromptAutocmp from .key_mappings import key_map +from .color_pairs import ColorPairs # Curses can't be imported yet but we'll # predefine it to avoid confusing flake8 @@ -145,8 +146,8 @@ def run(self, func): def load(self, *args): """Setup curses.""" # Log the terminal type - termname = curses.termname().decode("utf-8") - self.logger.debug("Loading UI for terminal: {0}".format(termname)) + self.termname = curses.termname().decode("utf-8") + self.logger.debug("Loading UI for terminal: {0}".format(self.termname)) self.screen = curses.initscr() self.setup_colors() @@ -185,70 +186,52 @@ def setup_mouse(self): def setup_colors(self): """Initialize color support and define colors.""" curses.start_color() + + self.logger.info( + "Currently running with TERM '%s' which provides %i colors and %i color pairs according to ncurses." % + (self.termname, curses.COLORS, curses.COLOR_PAIRS) + ) + + if curses.COLORS == 8: + self.logger.info("Enhanced colors not supported.") + self.logger.info( + "Depending on your terminal emulator 'export TERM=%s-256color' may help." % + self.termname + ) + self.app.config["editor"]["theme"] = "8colors" + + self.colors = ColorPairs() try: curses.use_default_colors() except: - self.logger.warning("Failed to load curses default colors. You could try 'export TERM=xterm-256color'.") - return False + self.logger.debug("Failed to load curses default colors.") + self.colors.set_default_fg(curses.COLOR_WHITE) + self.colors.set_default_bg(curses.COLOR_BLACK) + + colors = self._get_config_colors() + for key in colors: + values = colors[key] + self.colors.add_translate( + key, + values.get('fg', None), + values.get('bg', None), + values.get('attribs', None) + ) - # Default foreground color (could also be set to curses.COLOR_WHITE) - fg = -1 - # Default background color (could also be set to curses.COLOR_BLACK) - bg = -1 - - # This gets colors working in TTY's as well as terminal emulators - # curses.init_pair(10, -1, -1) # Default (white on black) - # Colors for xterm (not xterm-256color) - # Dark Colors - curses.init_pair(0, curses.COLOR_BLACK, bg) # 0 Black - curses.init_pair(1, curses.COLOR_RED, bg) # 1 Red - curses.init_pair(2, curses.COLOR_GREEN, bg) # 2 Green - curses.init_pair(3, curses.COLOR_YELLOW, bg) # 3 Yellow - curses.init_pair(4, curses.COLOR_BLUE, bg) # 4 Blue - curses.init_pair(5, curses.COLOR_MAGENTA, bg) # 5 Magenta - curses.init_pair(6, curses.COLOR_CYAN, bg) # 6 Cyan - curses.init_pair(7, fg, bg) # 7 White on Black - curses.init_pair(8, fg, curses.COLOR_BLACK) # 8 White on Black (Line number color) - - # Set color for whitespace - # Fails on default Ubuntu terminal with $TERM=xterm (max 8 colors) - # TODO: Smarter implementation for custom colors - try: - curses.init_pair(9, 8, bg) # Gray (Whitespace color) - self.limited_colors = False - except: - # Try to revert the color - self.limited_colors = True - try: - curses.init_pair(9, fg, bg) # Try to revert color if possible - except: - # Reverting failed - self.logger.error("Failed to set and revert extra colors.") + self.app.themes.use(self.app.config["editor"]["theme"]) - # Nicer shades of same colors (if supported) - if curses.can_change_color(): - try: - # TODO: Define RGB for these to avoid getting - # different results in different terminals - # xterm-256color chart http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html - curses.init_pair(0, 242, bg) # 0 Black - curses.init_pair(1, 204, bg) # 1 Red - curses.init_pair(2, 119, bg) # 2 Green - curses.init_pair(3, 221, bg) # 3 Yellow - curses.init_pair(4, 69, bg) # 4 Blue - curses.init_pair(5, 171, bg) # 5 Magenta - curses.init_pair(6, 81, bg) # 6 Cyan - curses.init_pair(7, 15, bg) # 7 White - curses.init_pair(8, 8, curses.COLOR_BLACK) # 8 Gray on Black (Line number color) - curses.init_pair(9, 8, bg) # 8 Gray (Whitespace color) - except: - self.logger.info("Enhanced colors failed to load. You could try 'export TERM=xterm-256color'.") - self.app.config["editor"]["theme"] = "8colors" + def _get_config_colors(self): + if curses.COLORS == 8: + return self.app.config["display"]["colors_8"] + elif curses.COLORS == 88: + return self.app.config["display"]["colors_88"] + elif curses.COLORS == 256: + return self.app.config["display"]["colors_256"] else: - self.logger.info("Enhanced colors not supported. You could try 'export TERM=xterm-256color'.") - self.app.config["editor"]["theme"] = "8colors" - - self.app.themes.use(self.app.config["editor"]["theme"]) + self.logger.warning( + "No idea how to handle a color count of %i. Defaulting to 8 colors." % curses.COLORS + ) + return self.app.config["display"]["colors_8"] def setup_windows(self): """Initialize and layout windows.""" @@ -262,7 +245,6 @@ def setup_windows(self): # https://anonscm.debian.org/cgit/collab-maint/ncurses.git/tree/ncurses/base/resizeterm.c#n274 # https://anonscm.debian.org/cgit/collab-maint/ncurses.git/tree/ncurses/base/wresize.c#n87 self.text_input = None - offset_top = 0 offset_bottom = 0 y, x = self.screen.getmaxyx() @@ -275,6 +257,7 @@ def setup_windows(self): elif self.header_win.getmaxyx()[1] != x: # Header bar don't ever need to move self.header_win.resize(1, x) + self.header_win.bkgdset(" ", self.colors.get("status_top")) if config["show_bottom_bar"]: offset_bottom += 1 @@ -284,6 +267,7 @@ def setup_windows(self): self.status_win.mvwin(y - offset_bottom, 0) if self.status_win.getmaxyx()[1] != x: self.status_win.resize(1, x) + self.status_win.bkgdset(" ", self.colors.get("status_bottom")) if config["show_legend"]: offset_bottom += 2 @@ -303,6 +287,7 @@ def setup_windows(self): self.app.get_editor().move_win((offset_top, 0)) # self.editor_win.mvwin(offset_top, 0) # self.editor_win.resize(y - offset_top - offset_bottom, x) + self.editor_win.bkgdset(" ", self.colors.get("editor")) def get_size(self): """Get terminal size.""" @@ -371,10 +356,7 @@ def show_top_status(self): if head_width > size[0]: head = head[:size[0]-head_width] try: - if self.app.config["display"]["invert_status_bars"]: - self.header_win.addstr(0, 0, head, curses.color_pair(0) | curses.A_REVERSE) - else: - self.header_win.addstr(0, 0, head, curses.color_pair(0)) + self.header_win.addstr(0, 0, head) except curses.error: pass self.header_win.refresh() @@ -429,18 +411,13 @@ def show_bottom_status(self): if len(line) >= size[0]: line = line[:size[0]-1] - if self.app.config["display"]["invert_status_bars"]: - attrs = curses.color_pair(0) | curses.A_REVERSE - else: - attrs = curses.color_pair(0) - # This thwarts a weird crash that happens when pasting a lot # of data that contains line breaks into the find dialog. # Should probably figure out why it happens, but it's not # due to line breaks in the data nor is the data too long. # Thanks curses! try: - self.status_win.addstr(0, 0, line, attrs) + self.status_win.addstr(0, 0, line) except: self.logger.exception("Failed to show bottom status bar. Status line was: {0}".format(line)) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 15dde1f..93eb9f7 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -327,8 +327,12 @@ def render(self): self.window.bkgdset(" ", attribs | curses.A_BOLD) if self.config["show_line_nums"]: - curs_color = curses.color_pair(line.number_color) padded_num = "{:{}{}d} ".format(lnum + 1, lnum_pad, lnum_len) + curs_color = self.app.ui.colors.get("linenumbers") + if line.state: + state_style = self.app.ui.colors.get_alt("linenumbers_" + line.state, None) + if state_style is not None: + curs_color = state_style self.window.addstr(i, 0, padded_num, curs_color) pos = (x_offset, i) @@ -394,14 +398,16 @@ def render_line_pygments(self, line, pos, x_offset, max_len): break scope = token[0] text = self.replace_whitespace(token[1]) - if token[1].isspace() and not self.app.ui.limited_colors: - pair = 9 # Default to gray text on normal background - settings = self.app.themes.get_scope("global") - if settings and settings.get("invisibles"): - fg = int(settings.get("invisibles") or -1) - bg = int(settings.get("background") or -1) - curses.init_pair(pair, fg, bg) - curs_color = curses.color_pair(pair) + if token[1].isspace(): + curs_color = self.app.ui.colors.get("editor_whitespace") + if not self.config["ignore_theme_whitespace"]: + settings = self.app.themes.get_scope("global") + if settings and settings.get("invisibles"): + curs_color = self.app.ui.colors.get_alt("syntax_pyg_whitespace", None) + if curs_color is None: + fg = int(settings.get("invisibles") or self.app.ui.colors.get_fg("editor")) + bg = int(settings.get("background") or self.app.ui.colors.get_bg("editor")) + curs_color = self.app.ui.colors.add_curses("syntax_pyg_whitespace", fg, bg) # Only add tab indicators to the inital whitespace if first_token and self.config["show_tab_indicators"]: text = self.add_tab_indicators(text) @@ -413,10 +419,12 @@ def render_line_pygments(self, line, pos, x_offset, max_len): self.logger.info("Theme settings for scope '{0}' of word '{1}' not found.".format(scope, token[1])) pair = scope_to_pair.get(scope) if settings and pair is not None: - fg = int(settings.get("foreground") or -1) - bg = int(settings.get("background") or -1) - curses.init_pair(pair, fg, bg) - curs_color = curses.color_pair(pair) + pair = "syntax_pyg_%s" % pair + curs_color = self.app.ui.colors.get_alt(pair, None) + if curs_color is None: + fg = int(settings.get("foreground") or self.app.ui.colors.get_fg("editor")) + bg = int(settings.get("background") or self.app.ui.colors.get_bg("editor")) + curs_color = self.app.ui.colors.add_curses(pair, fg, bg) self.window.addstr(y, x_offset, text, curs_color) else: self.window.addstr(y, x_offset, text) @@ -432,7 +440,11 @@ def render_line_linelight(self, line, pos, x_offset, max_len): y = pos[1] line_data = line.get_data() line_data = self._prepare_line_for_rendering(line_data, max_len) - curs_color = curses.color_pair(self.get_line_color(line)) + pair_fg = self.get_line_color(line) + pair = "syntax_ll_%s" % pair_fg + curs_color = self.app.ui.colors.get_alt(pair, None) + if curs_color is None: + curs_color = self.app.ui.colors.add_curses(pair, pair_fg, self.app.ui.colors.get_bg("editor")) self.window.addstr(y, x_offset, line_data, curs_color) def render_line_normal(self, line, pos, x_offset, max_len): From 1e2968feb3e55b1fb0deb7a5d2ee04c34afe6886 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 12:49:14 +0000 Subject: [PATCH 03/14] viewer.py: for linelight return default editor fg in case nothing matched --- suplemon/viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/viewer.py b/suplemon/viewer.py index 93eb9f7..057dac8 100644 --- a/suplemon/viewer.py +++ b/suplemon/viewer.py @@ -1026,4 +1026,4 @@ def get_line_color(self, raw_line): color = self.syntax.get_color(raw_line) if color is not None: return color - return 0 + return self.app.ui.colors.get_fg("editor") From 588bc28a758b95976b1caa5ba4dd432bad875e73 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 22:42:31 +0000 Subject: [PATCH 04/14] color_pairs.py: invalid colors: fall back to COLOR_WHITE if color count < 8 --- suplemon/color_pairs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/color_pairs.py b/suplemon/color_pairs.py index 68d6a5c..4b6df1a 100644 --- a/suplemon/color_pairs.py +++ b/suplemon/color_pairs.py @@ -17,7 +17,7 @@ def __init__(self): self._color_count = 1 # dynamic in case terminal does not support use_default_colors() - self._invalid = curses.COLOR_RED + self._invalid = curses.COLOR_WHITE if curses.COLORS < 8 else curses.COLOR_RED self._default_fg = -1 self._default_bg = -1 From cc04471e0e932a8e30f6f578958c08e12aa630aa Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 22:51:19 +0000 Subject: [PATCH 05/14] ui.py: warn user about missing transparency and default colors --- suplemon/ui.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/suplemon/ui.py b/suplemon/ui.py index 00e1c6c..d20bfe3 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -204,7 +204,12 @@ def setup_colors(self): try: curses.use_default_colors() except: - self.logger.debug("Failed to load curses default colors.") + self.logger.warning( + "Failed to load curses default colors. " + + "You will have no transparency or terminal defined default colors." + ) + # https://docs.python.org/3/library/curses.html#curses.init_pair + # "[..] the 0 color pair is wired to white on black and cannot be changed" self.colors.set_default_fg(curses.COLOR_WHITE) self.colors.set_default_bg(curses.COLOR_BLACK) From 0cd359f70d1f795675d697ae3389652acb44cb07 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 22:57:13 +0000 Subject: [PATCH 06/14] fixup --- suplemon/color_pairs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/color_pairs.py b/suplemon/color_pairs.py index 4b6df1a..804c93d 100644 --- a/suplemon/color_pairs.py +++ b/suplemon/color_pairs.py @@ -98,7 +98,7 @@ def add_curses(self, name, fg, bg, attrs=0): else: self.logger.warning( "Failed to create new color pair for " + - "'%s', the terminal description for '%s' only supports up to %i color pairs" % + "'%s', the terminal description for '%s' only supports up to %i color pairs." % (name, curses.termname().decode("utf-8"), curses.COLOR_PAIRS) ) color = curses.color_pair(0) | attrs From 0f6efc8c8205a96c51012f43908db98fcd4eca0a Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 23:05:57 +0000 Subject: [PATCH 07/14] color_pairs.py: change logging level from info to debug --- suplemon/color_pairs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/suplemon/color_pairs.py b/suplemon/color_pairs.py index 804c93d..df935a1 100644 --- a/suplemon/color_pairs.py +++ b/suplemon/color_pairs.py @@ -88,11 +88,11 @@ def add_curses(self, name, fg, bg, attrs=0): if name in self._colors: # Redefine exiting color pair index, color, fg, bg, attrs = self._colors[name] - self.logger.info("Updating exiting color pair with index %i and name '%s'" % (index, name)) + self.logger.debug("Updating exiting color pair with index %i and name '%s'" % (index, name)) else: # Create new color pair index = self._color_count - self.logger.info("Creating new color pair with index %i and name '%s'" % (index, name)) + self.logger.debug("Creating new color pair with index %i and name '%s'" % (index, name)) if index < curses.COLOR_PAIRS: self._color_count += 1 else: From e8c798fe2f1fa3d01bfb1d216a34e9add1b4966c Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 23:36:29 +0000 Subject: [PATCH 08/14] feature/color_manager: add legend color pair --- suplemon/config/defaults.json | 3 +++ suplemon/ui.py | 1 + 2 files changed, 4 insertions(+) diff --git a/suplemon/config/defaults.json b/suplemon/config/defaults.json index c7b77d5..ed48227 100644 --- a/suplemon/config/defaults.json +++ b/suplemon/config/defaults.json @@ -146,6 +146,7 @@ // Another variant for linenumbers (and maybe status_*) is black, black, bold "status_top": { "fg": "white", "bg": "black" }, "status_bottom": { "fg": "white", "bg": "black" }, + "legend": { "fg": "white", "bg": "black" }, "linenumbers": { "fg": "white", "bg": "black" }, "linenumbers_lint_error": { "fg": "red", "bg": "black" }, "editor": { "fg": "default", "bg": "default" }, @@ -156,6 +157,7 @@ // Copy of colors_8; this needs an own default theme "status_top": { "fg": "white", "bg": "black" }, "status_bottom": { "fg": "white", "bg": "black" }, + "legend": { "fg": "white", "bg": "black" }, "linenumbers": { "fg": "white", "bg": "black" }, "linenumbers_lint_error": { "fg": "red", "bg": "black" }, "editor": { "fg": "default", "bg": "default" }, @@ -165,6 +167,7 @@ "colors_256": { "status_top": { "fg": "color250", "bg": "black" }, "status_bottom": { "fg": "color250", "bg": "black" }, + "legend": { "fg": "color250", "bg": "black" }, "linenumbers": { "fg": "color240", "bg": "black" }, "linenumbers_lint_error": { "fg": "color204", "bg": "black" }, "editor": { "fg": "default", "bg": "default" }, diff --git a/suplemon/ui.py b/suplemon/ui.py index d20bfe3..65180a3 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -282,6 +282,7 @@ def setup_windows(self): self.legend_win.mvwin(y - offset_bottom, 0) if self.legend_win.getmaxyx()[1] != x: self.legend_win.resize(2, x) + self.legend_win.bkgdset(" ", self.colors.get("legend")) if self.editor_win is None: self.editor_win = curses.newwin(y - offset_top - offset_bottom, x, offset_top, 0) From 3237959e1d90e546352f551fd530b95780a0c5c1 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Tue, 9 Jan 2018 23:41:47 +0000 Subject: [PATCH 09/14] ui.py: remove limited_colors as colors are now dynamic --- suplemon/ui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/suplemon/ui.py b/suplemon/ui.py index 65180a3..f0d4595 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -108,7 +108,6 @@ class UI: def __init__(self, app): self.app = app self.logger = logging.getLogger(__name__) - self.limited_colors = True self.screen = None self.current_yx = None self.text_input = None From f899c94b589f3501c23190f94bfc981d9fc7f8be Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Thu, 25 Jan 2018 15:48:24 +0000 Subject: [PATCH 10/14] feature/colormanager: move setup to color_pairs.py, add config binding --- suplemon/color_pairs.py | 85 ++++++++++++++++++++++++++++++++++++----- suplemon/ui.py | 59 +--------------------------- 2 files changed, 78 insertions(+), 66 deletions(-) diff --git a/suplemon/color_pairs.py b/suplemon/color_pairs.py index df935a1..a490890 100644 --- a/suplemon/color_pairs.py +++ b/suplemon/color_pairs.py @@ -7,19 +7,78 @@ import logging -class ColorPairs: - def __init__(self): - self.logger = logging.getLogger(__name__ + "." + ColorPairs.__name__) +class ColorManager: + def __init__(self, app): + self._app = app + self.logger = logging.getLogger(__name__ + "." + ColorManager.__name__) self._colors = dict() # color_pair(0) is hardcoded # https://docs.python.org/3/library/curses.html#curses.init_pair self._color_count = 1 + self._invalid = curses.COLOR_WHITE if curses.COLORS < 8 else curses.COLOR_RED # dynamic in case terminal does not support use_default_colors() - self._invalid = curses.COLOR_WHITE if curses.COLORS < 8 else curses.COLOR_RED self._default_fg = -1 self._default_bg = -1 + self._setup_colors() + self._load_color_theme() + self._app.set_event_binding("config_loaded", "after", self._load_color_theme) + + def _setup_colors(self): + """Initialize color support and define colors.""" + curses.start_color() + + self.termname = curses.termname().decode('utf-8') + self.logger.info( + "Currently running with TERM '%s' which provides %i colors and %i color pairs according to ncurses." % + (self.termname, curses.COLORS, curses.COLOR_PAIRS) + ) + + if curses.COLORS == 8: + self.logger.info("Enhanced colors not supported.") + self.logger.info( + "Depending on your terminal emulator 'export TERM=%s-256color' may help." % + self.termname + ) + self._app.config["editor"]["theme"] = "8colors" + + try: + curses.use_default_colors() + except: + self.logger.warning( + "Failed to load curses default colors. " + + "You will have no transparency or terminal defined default colors." + ) + # https://docs.python.org/3/library/curses.html#curses.init_pair + # "[..] the 0 color pair is wired to white on black and cannot be changed" + self.set_default_fg(curses.COLOR_WHITE) + self.set_default_bg(curses.COLOR_BLACK) + + def _load_color_theme(self, *args): + colors = self._get_config_colors() + for key in colors: + values = colors[key] + self.add_translate( + key, + values.get('fg', None), + values.get('bg', None), + values.get('attribs', None) + ) + self._app.themes.use(self._app.config["editor"]["theme"]) + + def _get_config_colors(self): + if curses.COLORS == 8: + return self._app.config["display"]["colors_8"] + elif curses.COLORS == 88: + return self._app.config["display"]["colors_88"] + elif curses.COLORS == 256: + return self._app.config["display"]["colors_256"] + else: + self.logger.warning( + "No idea how to handle a color count of %i. Defaulting to 8 colors." % curses.COLORS + ) + return self._app.config["display"]["colors_8"] def set_default_fg(self, color): self._default_fg = color @@ -87,19 +146,27 @@ def add_curses(self, name, fg, bg, attrs=0): name = str(name) if name in self._colors: # Redefine exiting color pair - index, color, fg, bg, attrs = self._colors[name] - self.logger.debug("Updating exiting color pair with index %i and name '%s'" % (index, name)) + index, color, _fg, _bg, _attrs = self._colors[name] + self.logger.debug( + "Updating exiting color pair with index %i, name '%s', fg=%i, bg=%i and attrs=%i" % ( + index, name, fg, bg, attrs + ) + ) else: # Create new color pair index = self._color_count - self.logger.debug("Creating new color pair with index %i and name '%s'" % (index, name)) + self.logger.debug( + "Creating new color pair with index %i, name '%s', fg=%i, bg=%i and attrs=%i" % ( + index, name, fg, bg, attrs + ) + ) if index < curses.COLOR_PAIRS: self._color_count += 1 else: self.logger.warning( "Failed to create new color pair for " + "'%s', the terminal description for '%s' only supports up to %i color pairs." % - (name, curses.termname().decode("utf-8"), curses.COLOR_PAIRS) + (name, self.termname, curses.COLOR_PAIRS) ) color = curses.color_pair(0) | attrs self._colors[name] = (0, color, curses.COLOR_WHITE, curses.COLOR_BLACK, attrs) @@ -146,7 +213,7 @@ def translate_color(self, color, check_for=None): if color_i >= curses.COLORS: self.logger.warning( "The terminal description for '%s' does not support more than %i colors. Specified color was %s" % - (curses.termname().decode("utf-8"), curses.COLORS, color) + (self.termname, curses.COLORS, color) ) return self._invalid diff --git a/suplemon/ui.py b/suplemon/ui.py index f0d4595..fce7372 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -10,7 +10,7 @@ from .prompt import Prompt, PromptBool, PromptFiltered, PromptFile, PromptAutocmp from .key_mappings import key_map -from .color_pairs import ColorPairs +from .color_pairs import ColorManager # Curses can't be imported yet but we'll # predefine it to avoid confusing flake8 @@ -149,7 +149,7 @@ def load(self, *args): self.logger.debug("Loading UI for terminal: {0}".format(self.termname)) self.screen = curses.initscr() - self.setup_colors() + self.colors = ColorManager(self.app) curses.raw() curses.noecho() @@ -182,61 +182,6 @@ def setup_mouse(self): else: curses.mousemask(0) # All events - def setup_colors(self): - """Initialize color support and define colors.""" - curses.start_color() - - self.logger.info( - "Currently running with TERM '%s' which provides %i colors and %i color pairs according to ncurses." % - (self.termname, curses.COLORS, curses.COLOR_PAIRS) - ) - - if curses.COLORS == 8: - self.logger.info("Enhanced colors not supported.") - self.logger.info( - "Depending on your terminal emulator 'export TERM=%s-256color' may help." % - self.termname - ) - self.app.config["editor"]["theme"] = "8colors" - - self.colors = ColorPairs() - try: - curses.use_default_colors() - except: - self.logger.warning( - "Failed to load curses default colors. " + - "You will have no transparency or terminal defined default colors." - ) - # https://docs.python.org/3/library/curses.html#curses.init_pair - # "[..] the 0 color pair is wired to white on black and cannot be changed" - self.colors.set_default_fg(curses.COLOR_WHITE) - self.colors.set_default_bg(curses.COLOR_BLACK) - - colors = self._get_config_colors() - for key in colors: - values = colors[key] - self.colors.add_translate( - key, - values.get('fg', None), - values.get('bg', None), - values.get('attribs', None) - ) - - self.app.themes.use(self.app.config["editor"]["theme"]) - - def _get_config_colors(self): - if curses.COLORS == 8: - return self.app.config["display"]["colors_8"] - elif curses.COLORS == 88: - return self.app.config["display"]["colors_88"] - elif curses.COLORS == 256: - return self.app.config["display"]["colors_256"] - else: - self.logger.warning( - "No idea how to handle a color count of %i. Defaulting to 8 colors." % curses.COLORS - ) - return self.app.config["display"]["colors_8"] - def setup_windows(self): """Initialize and layout windows.""" # We are using curses.newwin instead of self.screen.subwin/derwin because From 41fd0b7908e14aac19ca3e482ece19820e01ffaf Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Thu, 25 Jan 2018 15:51:42 +0000 Subject: [PATCH 11/14] feature/colormanager: rename color_pairs to color_manager --- suplemon/{color_pairs.py => color_manager.py} | 0 suplemon/ui.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename suplemon/{color_pairs.py => color_manager.py} (100%) diff --git a/suplemon/color_pairs.py b/suplemon/color_manager.py similarity index 100% rename from suplemon/color_pairs.py rename to suplemon/color_manager.py diff --git a/suplemon/ui.py b/suplemon/ui.py index fce7372..5add58f 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -10,7 +10,7 @@ from .prompt import Prompt, PromptBool, PromptFiltered, PromptFile, PromptAutocmp from .key_mappings import key_map -from .color_pairs import ColorManager +from .color_manager import ColorManager # Curses can't be imported yet but we'll # predefine it to avoid confusing flake8 From 77848db9f13f112f6259cbe980a8285642090ee8 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Thu, 8 Mar 2018 09:06:12 +0000 Subject: [PATCH 12/14] [WIP] fix all fixmes and todos --- suplemon/color_manager.py | 96 +++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 33 deletions(-) diff --git a/suplemon/color_manager.py b/suplemon/color_manager.py index a490890..c7911aa 100644 --- a/suplemon/color_manager.py +++ b/suplemon/color_manager.py @@ -5,7 +5,7 @@ import curses import logging - +from traceback import format_stack class ColorManager: def __init__(self, app): @@ -16,7 +16,8 @@ def __init__(self, app): # color_pair(0) is hardcoded # https://docs.python.org/3/library/curses.html#curses.init_pair self._color_count = 1 - self._invalid = curses.COLOR_WHITE if curses.COLORS < 8 else curses.COLOR_RED + self._invalid_fg = curses.COLOR_WHITE + self._invalid_bg = curses.COLOR_BLACK if curses.COLORS < 8 else curses.COLOR_RED # dynamic in case terminal does not support use_default_colors() self._default_fg = -1 @@ -52,8 +53,8 @@ def _setup_colors(self): ) # https://docs.python.org/3/library/curses.html#curses.init_pair # "[..] the 0 color pair is wired to white on black and cannot be changed" - self.set_default_fg(curses.COLOR_WHITE) - self.set_default_bg(curses.COLOR_BLACK) + self._set_default_fg(curses.COLOR_WHITE) + self._set_default_bg(curses.COLOR_BLACK) def _load_color_theme(self, *args): colors = self._get_config_colors() @@ -80,10 +81,10 @@ def _get_config_colors(self): ) return self._app.config["display"]["colors_8"] - def set_default_fg(self, color): + def _set_default_fg(self, color): self._default_fg = color - def set_default_bg(self, color): + def _set_default_bg(self, color): self._default_bg = color def _get(self, name, index=None, default=None, log_missing=True): @@ -96,7 +97,6 @@ def _get(self, name, index=None, default=None, log_missing=True): return ret[index] return ret - # FIXME: evaluate default returns on error. none? exception? ._default_[fb]g? .invalid? hardcoded? pair(0)? def get(self, name): """ Return colorpair ORed attribs or a fallback """ return self._get(name, index=1, default=curses.color_pair(0)) @@ -106,12 +106,12 @@ def get_alt(self, name, alt): return self._get(name, index=1, default=alt, log_missing=False) def get_fg(self, name): - """ Return foreground color as integer """ - return self._get(name, index=2, default=curses.COLOR_WHITE) + """ Return foreground color as integer or hardcoded invalid_fg (white) as fallback """ + return self._get(name, index=2, default=self._invalid_fg) def get_bg(self, name): - """ Return background color as integer """ - return self._get(name, index=3, default=curses.COLOR_RED) + """ Return background color as integer or hardcoded invalid_bg (red) as fallback""" + return self._get(name, index=3, default=self._invalid_bg) def get_color(self, name): """ Alternative for get(name) """ @@ -124,31 +124,34 @@ def get_all(self, name): return (None, None, None, None) return ret[1:] - def contains(self, name): + def __contains__(self, name): + """ Check if a color pair with this name exists """ return str(name) in self._colors def add_translate(self, name, fg, bg, attributes=None): """ Store or update color definition. fg and bg can be of form "blue" or "color162". - attributes can be a list of attribute names like "bold" or "underline". + attributes can be a list of attribute names like ["bold", "underline"]. """ return self.add_curses( name, - self.translate_color(fg, check_for="fg"), - self.translate_color(bg, check_for="bg"), - self.translate_attributes(attributes) + self._translate_color(fg, usage_hint="fg"), + self._translate_color(bg, usage_hint="bg"), + self._translate_attributes(attributes) ) def add_curses(self, name, fg, bg, attrs=0): - """ Store or update color definition. fg, bg and attrs must be valid curses values """ - # FIXME: catch invalid colors, attrs,... + """ + Store or update color definition. + fg, bg and attrs must be valid curses values. + """ name = str(name) if name in self._colors: - # Redefine exiting color pair + # Redefine existing color pair index, color, _fg, _bg, _attrs = self._colors[name] self.logger.debug( - "Updating exiting color pair with index %i, name '%s', fg=%i, bg=%i and attrs=%i" % ( + "Updating exiting curses color pair with index %i, name '%s', fg=%s, bg=%s and attrs=%s" % ( index, name, fg, bg, attrs ) ) @@ -156,7 +159,7 @@ def add_curses(self, name, fg, bg, attrs=0): # Create new color pair index = self._color_count self.logger.debug( - "Creating new color pair with index %i, name '%s', fg=%i, bg=%i and attrs=%i" % ( + "Creating new curses color pair with index %i, name '%s', fg=%s, bg=%s and attrs=%s" % ( index, name, fg, bg, attrs ) ) @@ -164,19 +167,33 @@ def add_curses(self, name, fg, bg, attrs=0): self._color_count += 1 else: self.logger.warning( - "Failed to create new color pair for " + + "Failed to create new color pair for " "'%s', the terminal description for '%s' only supports up to %i color pairs." % (name, self.termname, curses.COLOR_PAIRS) ) - color = curses.color_pair(0) | attrs + try: + color = curses.color_pair(0) | attrs + except: + self.logger.warning("Invalid attributes: '%s'" % str(attrs)) + color = curses.color_pair(0) self._colors[name] = (0, color, curses.COLOR_WHITE, curses.COLOR_BLACK, attrs) return color - curses.init_pair(index, fg, bg) - color = curses.color_pair(index) | attrs + try: + curses.init_pair(index, fg, bg) + color = curses.color_pair(index) | attrs + except Exception as e: + self.logger.warning( + "Failed to create or update curses color pair with " + "index %i, name '%s', fg=%s, bg=%s, attrs=%s. error was: %s" % + (index, name, fg, bg, str(attrs), e) + ) + color = curses.color_pair(0) + self._colors[name] = (index, color, fg, bg, attrs) return color - def translate_attributes(self, attributes): + def _translate_attributes(self, attributes): + """ Translate list of attributes into native curses format """ if attributes is None: return 0 val = 0 @@ -184,9 +201,13 @@ def translate_attributes(self, attributes): val |= getattr(curses, "A_" + attrib.upper(), 0) return val - def translate_color(self, color, check_for=None): + def _translate_color(self, color, usage_hint=None): + """ + Translate color name of form 'blue' or 'color252' into native curses format. + On error return hardcoded invalid_fg or _bg (white or red) color. + """ if color is None: - return self._invalid + return self._invalid_fg if usage_hint == "fg" else self._invalid_bg color_i = getattr(curses, "COLOR_" + color.upper(), None) if color_i is not None: @@ -194,27 +215,36 @@ def translate_color(self, color, check_for=None): color = color.lower() if color == "default": - # FIXME: what to return if check_for is not set? - return self._default_fg if check_for == "fg" else self._default_bg + if usage_hint == "fg": + return self._default_fg + elif usage_hint == "bg": + return self._default_bg + else: + self.logger.warning("Default color requested without usage_hint being one of fg, bg.") + self.logger.warning("This is likely a bug, please report at https://github.com/richrd/suplemon/issues") + self.logger.warning("and include the following stacktrace.") + for line in format_stack()[:-1]: + self.logger.warning(line.strip()) + return self._invalid_bg elif color.startswith("color"): color_i = color[len("color"):] elif color.startswith("colour"): color_i = color[len("colour"):] else: self.logger.warning("Invalid color specified: '%s'" % color) - return self._invalid + return self._invalid_fg if usage_hint == "fg" else self._invalid_bg try: color_i = int(color_i) except: self.logger.warning("Invalid color specified: '%s'" % color) - return self._invalid + return self._invalid_fg if usage_hint == "fg" else self._invalid_bg if color_i >= curses.COLORS: self.logger.warning( "The terminal description for '%s' does not support more than %i colors. Specified color was %s" % (self.termname, curses.COLORS, color) ) - return self._invalid + return self._invalid_fg if usage_hint == "fg" else self._invalid_bg return color_i From 6d1bf5e541b9b085afdba71c1f10b397819c4e43 Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Thu, 8 Mar 2018 09:09:40 +0000 Subject: [PATCH 13/14] [WIP] rename Python file from color_manager.py to color_manager_curses.py --- suplemon/{color_manager.py => color_manager_curses.py} | 0 suplemon/ui.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename suplemon/{color_manager.py => color_manager_curses.py} (100%) diff --git a/suplemon/color_manager.py b/suplemon/color_manager_curses.py similarity index 100% rename from suplemon/color_manager.py rename to suplemon/color_manager_curses.py diff --git a/suplemon/ui.py b/suplemon/ui.py index 5add58f..622265a 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -10,7 +10,7 @@ from .prompt import Prompt, PromptBool, PromptFiltered, PromptFile, PromptAutocmp from .key_mappings import key_map -from .color_manager import ColorManager +from .color_manager_curses import ColorManager # Curses can't be imported yet but we'll # predefine it to avoid confusing flake8 From a57608c9f30942346721168d1d90db89b5527fab Mon Sep 17 00:00:00 2001 From: Consolatis <35009135+Consolatis@users.noreply.github.com> Date: Thu, 8 Mar 2018 09:13:10 +0000 Subject: [PATCH 14/14] [WIP] codestyle fixup --- suplemon/color_manager_curses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/suplemon/color_manager_curses.py b/suplemon/color_manager_curses.py index c7911aa..7820f3e 100644 --- a/suplemon/color_manager_curses.py +++ b/suplemon/color_manager_curses.py @@ -7,6 +7,7 @@ import logging from traceback import format_stack + class ColorManager: def __init__(self, app): self._app = app