From 6203a84553389fb933eddd2f5d0a90959972518a Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Wed, 7 Nov 2018 14:00:35 +0100 Subject: [PATCH 1/7] Use uniform spelling: colour -> color. --- matrix/colors.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/matrix/colors.py b/matrix/colors.py index 96a4c24..aa4a452 100644 --- a/matrix/colors.py +++ b/matrix/colors.py @@ -624,20 +624,20 @@ def color_line_to_weechat(color_string): return line_colors[color_string] -# The functions colour_dist_sq(), colour_to_6cube(), and colour_find_rgb +# The functions color_dist_sq(), color_to_6cube(), and color_find_rgb # are python ports of the same named functions from the tmux # source, they are under the copyright of Nicholas Marriott, and Avi Halachmi # under the ISC license. # More info: https://github.com/tmux/tmux/blob/master/colour.c -def colour_dist_sq(R, G, B, r, g, b): +def color_dist_sq(R, G, B, r, g, b): # pylint: disable=invalid-name,too-many-arguments # type: (int, int, int, int, int, int) -> int return (R - r) * (R - r) + (G - g) * (G - g) + (B - b) * (B - b) -def colour_to_6cube(v): +def color_to_6cube(v): # pylint: disable=invalid-name # type: (int) -> int if v < 48: @@ -647,15 +647,15 @@ def colour_to_6cube(v): return (v - 35) // 40 -def colour_find_rgb(r, g, b): +def color_find_rgb(r, g, b): # type: (int, int, int) -> int - """Convert an RGB triplet to the xterm(1) 256 colour palette. + """Convert an RGB triplet to the xterm(1) 256 color palette. - xterm provides a 6x6x6 colour cube (16 - 231) and 24 greys (232 - 255). - We map our RGB colour to the closest in the cube, also work out the + xterm provides a 6x6x6 color cube (16 - 231) and 24 greys (232 - 255). + We map our RGB color to the closest in the cube, also work out the closest grey, and use the nearest of the two. - Note that the xterm has much lower resolution for darker colours (they + Note that the xterm has much lower resolution for darker colors (they are not evenly spread out), so our 6 levels are not evenly spread: 0x0, 0x5f (95), 0x87 (135), 0xaf (175), 0xd7 (215) and 0xff (255). Greys are more evenly spread (8, 18, 28 ... 238). @@ -664,15 +664,15 @@ def colour_find_rgb(r, g, b): q2c = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] # Map RGB to 6x6x6 cube. - qr = colour_to_6cube(r) - qg = colour_to_6cube(g) - qb = colour_to_6cube(b) + qr = color_to_6cube(r) + qg = color_to_6cube(g) + qb = color_to_6cube(b) cr = q2c[qr] cg = q2c[qg] cb = q2c[qb] - # If we have hit the colour exactly, return early. + # If we have hit the color exactly, return early. if cr == r and cg == g and cb == b: return 16 + (36 * qr) + (6 * qg) + qb @@ -686,10 +686,10 @@ def colour_find_rgb(r, g, b): grey = 8 + (10 * grey_idx) - # Is grey or 6x6x6 colour closest? - d = colour_dist_sq(cr, cg, cb, r, g, b) + # Is grey or 6x6x6 color closest? + d = color_dist_sq(cr, cg, cb, r, g, b) - if colour_dist_sq(grey, grey, grey, r, g, b) < d: + if color_dist_sq(grey, grey, grey, r, g, b) < d: idx = 232 + grey_idx else: idx = 16 + (36 * qr) + (6 * qg) + qb @@ -728,7 +728,7 @@ def color_html_to_weechat(color): if rgb_color in weechat_basic_colors: return weechat_basic_colors[rgb_color] - return str(colour_find_rgb(*rgb_color)) + return str(color_find_rgb(*rgb_color)) def color_weechat_to_html(color): From 96e8f5246bca926bf16f1b7eb5263bef377540f6 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Mon, 12 Nov 2018 19:25:30 +0100 Subject: [PATCH 2/7] Improve handling of code blocks. * Don't try to guess the language of code blocks without a language specifier ("untagged" blocks). * Colour untagged and inline code blocks (and make the colour configurable). * Reflow inline code blocks. --- matrix/colors.py | 25 +++++++++++++++++++------ matrix/config.py | 10 ++++++++++ matrix/utils.py | 18 ++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/matrix/colors.py b/matrix/colors.py index aa4a452..eff7697 100644 --- a/matrix/colors.py +++ b/matrix/colors.py @@ -30,12 +30,12 @@ from typing import List import webcolors from pygments import highlight from pygments.formatter import Formatter, get_style_by_name -from pygments.lexers import get_lexer_by_name, guess_lexer +from pygments.lexers import get_lexer_by_name from pygments.util import ClassNotFound from . import globals as G from .globals import W -from .utils import string_strikethrough +from .utils import string_strikethrough, string_color_and_reset try: from HTMLParser import HTMLParser @@ -302,7 +302,8 @@ class Formatted(object): try: lexer = get_lexer_by_name(value) except ClassNotFound: - lexer = guess_lexer(string) + return string_color_and_reset(string, + G.CONFIG.color.untagged_code) try: style = get_style_by_name(G.CONFIG.look.pygments_style) @@ -345,14 +346,21 @@ class Formatted(object): return prefix + text.replace("\n", "\n{}".format(prefix)) for key, value in attributes.items(): - # Don't use textwrap to quote the code - if attributes["code"] and key == "quote" and value: + if not value: continue + # Don't use textwrap to quote the code + if key == "quote" and attributes["code"]: + continue + + # Reflow inline code blocks + if key == "code" and not attributes["preformatted"]: + text = text.strip().replace('\n', ' ') + text = add_attribute(text, key, value) # If we're quoted code add quotation marks now. - if attributes["quote"] and key == "code" and value: + if key == "code" and attributes["quote"]: text = indent( text, "{}>{} ".format( @@ -382,6 +390,7 @@ DEFAULT_ATTRIBUTES = { "italic": False, "underline": False, "strikethrough": False, + "preformatted": False, "quote": False, "code": None, "fgcolor": None, @@ -430,6 +439,8 @@ class MatrixHtmlParser(HTMLParser): self._toggle_attribute("strikethrough") elif tag == "blockquote": self._toggle_attribute("quote") + elif tag == "pre": + self._toggle_attribute("preformatted") elif tag == "code": lang = None @@ -480,6 +491,8 @@ class MatrixHtmlParser(HTMLParser): self._toggle_attribute("underline") elif tag == "del": self._toggle_attribute("strikethrough") + elif tag == "pre": + self._toggle_attribute("preformatted") elif tag == "code": if self.text: self.add_substring(self.text, self.attributes.copy()) diff --git a/matrix/config.py b/matrix/config.py index 8914938..c8655f7 100644 --- a/matrix/config.py +++ b/matrix/config.py @@ -671,6 +671,16 @@ class MatrixConfig(WeechatConfig): ("Color for messages that are printed out but the server " "hasn't confirmed the that he received them."), ), + Option( + "untagged_code", + "color", + "", + 0, + 0, + "blue", + ("Color for code without a language specifier. Also used for " + "`inline code`."), + ), ] sections = [ diff --git a/matrix/utils.py b/matrix/utils.py index 060fd14..e711953 100644 --- a/matrix/utils.py +++ b/matrix/utils.py @@ -105,3 +105,21 @@ def shorten_sender(sender): def string_strikethrough(string): return "".join(["{}\u0336".format(c) for c in string]) + + +def string_color_and_reset(string, color): + """Color string with color, then reset all attributes.""" + + lines = string.split('\n') + lines = ("{}{}{}".format(W.color(color), line, W.color("reset")) + for line in lines) + return "\n".join(lines) + + +def string_color(string, color): + """Color string with color, then reset the color attribute.""" + + lines = string.split('\n') + lines = ("{}{}{}".format(W.color(color), line, W.color("resetcolor")) + for line in lines) + return "\n".join(lines) From 041c15e811ff48677e32f40c39d59be0330b5da4 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Mon, 12 Nov 2018 19:31:24 +0100 Subject: [PATCH 3/7] Be more careful when stripping newline added by pygments. Calls rstrip() instead of cutting of the last character blindly, so that: 1. Potentially multiple trailing whitespace of any kind will get stripped. 2. We don't chop off a non-whitespace character, in case pygments' changes. --- matrix/colors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix/colors.py b/matrix/colors.py index eff7697..eb6dece 100644 --- a/matrix/colors.py +++ b/matrix/colors.py @@ -313,7 +313,7 @@ class Formatted(object): # highlight adds a newline to the end of the string, remove it # from the output return highlight(string, lexer, - WeechatFormatter(style=style))[:-1] + WeechatFormatter(style=style)).rstrip() elif name == "fgcolor": return "{color_on}{text}{color_off}".format( color_on=W.color(value), From 141814bb848e04fb3f1ec64c8e6f10f760e39789 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Mon, 12 Nov 2018 22:42:44 +0100 Subject: [PATCH 4/7] Support full color pair (fg/bg) for each color. --- matrix/buffer.py | 11 +++++++-- matrix/colors.py | 22 +++++++++++------- matrix/config.py | 60 ++++++++++++++++++++++++++++++++++++++---------- matrix/server.py | 3 ++- matrix/utils.py | 9 ++++++++ 5 files changed, 82 insertions(+), 23 deletions(-) diff --git a/matrix/buffer.py b/matrix/buffer.py index 6c9ac18..cb10db5 100644 --- a/matrix/buffer.py +++ b/matrix/buffer.py @@ -53,7 +53,12 @@ from .colors import Formatted from .config import RedactType from .globals import SCRIPT_NAME, SERVERS, W, TYPING_NOTICE_TIMEOUT from .utf import utf8_decode -from .utils import server_ts_to_weechat, shorten_sender, string_strikethrough +from .utils import ( + server_ts_to_weechat, + shorten_sender, + string_strikethrough, + color_pair, +) OwnMessages = NamedTuple( "OwnMessages", @@ -1446,7 +1451,9 @@ class RoomBuffer(object): "message{del_color}>{ncolor}").format( del_color=W.color("chat_delimiters"), ncolor=W.color("reset"), - error_color=W.color(G.CONFIG.color.error_message)) + error_color=W.color(color_pair( + G.CONFIG.color.error_message_fg, + G.CONFIG.color.error_message_bg))) last_line.message = message diff --git a/matrix/colors.py b/matrix/colors.py index eb6dece..a8a7376 100644 --- a/matrix/colors.py +++ b/matrix/colors.py @@ -35,7 +35,9 @@ from pygments.util import ClassNotFound from . import globals as G from .globals import W -from .utils import string_strikethrough, string_color_and_reset +from .utils import (string_strikethrough, + string_color_and_reset, + color_pair) try: from HTMLParser import HTMLParser @@ -59,10 +61,12 @@ class Formatted(object): @property def textwrapper(self): + quote_pair = color_pair(G.CONFIG.color.quote_fg, + G.CONFIG.color.quote_bg) return textwrap.TextWrapper( width=67, - initial_indent="{}> ".format(W.color(G.CONFIG.color.quote)), - subsequent_indent="{}> ".format(W.color(G.CONFIG.color.quote)), + initial_indent="{}> ".format(W.color(quote_pair)), + subsequent_indent="{}> ".format(W.color(quote_pair)), ) def is_formatted(self): @@ -302,8 +306,10 @@ class Formatted(object): try: lexer = get_lexer_by_name(value) except ClassNotFound: - return string_color_and_reset(string, - G.CONFIG.color.untagged_code) + return string_color_and_reset( + string, + color_pair(G.CONFIG.color.untagged_code_fg, + G.CONFIG.color.untagged_code_bg)) try: style = get_style_by_name(G.CONFIG.look.pygments_style) @@ -361,11 +367,11 @@ class Formatted(object): # If we're quoted code add quotation marks now. if key == "code" and attributes["quote"]: + fg = G.CONFIG.color.quote_fg + bg = G.CONFIG.color.quote_bg text = indent( text, - "{}>{} ".format( - W.color(G.CONFIG.color.quote), W.color("reset") - ), + string_color_and_reset(">", color_pair(fg, bg)) + " ", ) # If we're code don't remove multiple newlines blindly diff --git a/matrix/config.py b/matrix/config.py index c8655f7..98f51c2 100644 --- a/matrix/config.py +++ b/matrix/config.py @@ -642,44 +642,80 @@ class MatrixConfig(WeechatConfig): color_options = [ Option( - "quote", + "quote_fg", "color", "", 0, 0, "lightgreen", - ("Color for matrix style blockquotes"), + "Foreground color for matrix style blockquotes", ), Option( - "error_message", + "quote_bg", + "color", + "", + 0, + 0, + "default", + "Background counterpart of quote_fg", + ), + Option( + "error_message_fg", "color", "", 0, 0, "darkgray", - ("Color for error messages that appear inside a room buffer (" - "e.g. when a message errors out when sending or when a " - "message is redacted)"), + ("Foreground color for error messages that appear inside a " + "room buffer (e.g. when a message errors out when sending or " + "when a message is redacted)"), ), Option( - "unconfirmed_message", + "error_message_bg", + "color", + "", + 0, + 0, + "default", + "Background counterpart of error_message_fg.", + ), + Option( + "unconfirmed_message_fg", "color", "", 0, 0, "darkgray", - ("Color for messages that are printed out but the server " - "hasn't confirmed the that he received them."), + ("Foreground color for messages that are printed out but the " + "server hasn't confirmed the that he received them."), ), Option( - "untagged_code", + "unconfirmed_message_bg", + "color", + "", + 0, + 0, + "default", + "Background counterpart of unconfirmed_message_fg." + ), + Option( + "untagged_code_fg", "color", "", 0, 0, "blue", - ("Color for code without a language specifier. Also used for " - "`inline code`."), + ("Foreground color for code without a language specifier. " + "Also used for `inline code`."), + ), + Option( + "untagged_code_bg", + "color", + "", + 0, + 0, + "default", + "Background counterpart of untagged_code_fg", ), ] diff --git a/matrix/server.py b/matrix/server.py index eef4cab..5546ad1 100644 --- a/matrix/server.py +++ b/matrix/server.py @@ -798,7 +798,8 @@ class MatrixServer(object): plain_message = formatted.to_weechat() plain_message = W.string_remove_color(plain_message, "") attributes = DEFAULT_ATTRIBUTES.copy() - attributes["fgcolor"] = G.CONFIG.color.unconfirmed_message + attributes["fgcolor"] = G.CONFIG.color.unconfirmed_message_fg + attributes["bgcolor"] = G.CONFIG.color.unconfirmed_message_bg new_formatted = Formatted([FormattedString( plain_message, attributes diff --git a/matrix/utils.py b/matrix/utils.py index e711953..d77b6f6 100644 --- a/matrix/utils.py +++ b/matrix/utils.py @@ -123,3 +123,12 @@ def string_color(string, color): lines = ("{}{}{}".format(W.color(color), line, W.color("resetcolor")) for line in lines) return "\n".join(lines) + + +def color_pair(color_fg, color_bg): + """Make a color pair from a pair of colors.""" + + if color_bg: + return "{},{}".format(color_fg, color_bg) + else: + return color_fg From a6cf2e15272a554474ee13a571fce58e5c6af4c9 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Tue, 13 Nov 2018 10:28:44 +0100 Subject: [PATCH 5/7] Stylize code blocks as actual boxes with margins. --- matrix/colors.py | 60 +++++++++++++++++++++++++++++++++--------------- matrix/utils.py | 35 +++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/matrix/colors.py b/matrix/colors.py index a8a7376..218cfe2 100644 --- a/matrix/colors.py +++ b/matrix/colors.py @@ -37,7 +37,9 @@ from . import globals as G from .globals import W from .utils import (string_strikethrough, string_color_and_reset, - color_pair) + color_pair, + text_block, + colored_text_block) try: from HTMLParser import HTMLParser @@ -275,7 +277,7 @@ class Formatted(object): def to_weechat(self): # TODO BG COLOR - def add_attribute(string, name, value): + def add_attribute(string, name, value, attributes): if not value: return string elif name == "bold": @@ -303,23 +305,40 @@ class Formatted(object): W.string_remove_color(string.replace("\n", ""), "") ) elif name == "code": - try: - lexer = get_lexer_by_name(value) - except ClassNotFound: - return string_color_and_reset( - string, - color_pair(G.CONFIG.color.untagged_code_fg, - G.CONFIG.color.untagged_code_bg)) + code_color_pair = color_pair( + G.CONFIG.color.untagged_code_fg, + G.CONFIG.color.untagged_code_bg + ) - try: - style = get_style_by_name(G.CONFIG.look.pygments_style) - except ClassNotFound: - style = "native" + if attributes["preformatted"]: + # code block - # highlight adds a newline to the end of the string, remove it - # from the output - return highlight(string, lexer, - WeechatFormatter(style=style)).rstrip() + try: + lexer = get_lexer_by_name(value) + except ClassNotFound: + return colored_text_block( + string, + margin=2, + color_pair=code_color_pair) + + try: + style = get_style_by_name(G.CONFIG.look.pygments_style) + except ClassNotFound: + style = "native" + + code_block = text_block(string, margin=2) + + # highlight adds a newline to the end of the string, remove it + # from the output + highlighted_code = highlight( + code_block, + lexer, + WeechatFormatter(style=style) + ).rstrip() + + return highlighted_code + else: + return string_color_and_reset(string, code_color_pair) elif name == "fgcolor": return "{color_on}{text}{color_off}".format( color_on=W.color(value), @@ -344,7 +363,10 @@ class Formatted(object): # terminal, but doing it the other way around results in garbage. if "strikethrough" in attributes: text = add_attribute( - text, "strikethrough", attributes["strikethrough"] + text, + "strikethrough", + attributes["strikethrough"], + attributes ) attributes.pop("strikethrough") @@ -363,7 +385,7 @@ class Formatted(object): if key == "code" and not attributes["preformatted"]: text = text.strip().replace('\n', ' ') - text = add_attribute(text, key, value) + text = add_attribute(text, key, value, attributes) # If we're quoted code add quotation marks now. if key == "code" and attributes["quote"]: diff --git a/matrix/utils.py b/matrix/utils.py index d77b6f6..91d122d 100644 --- a/matrix/utils.py +++ b/matrix/utils.py @@ -14,7 +14,7 @@ # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -from __future__ import unicode_literals +from __future__ import unicode_literals, division import time from typing import Any, Dict, List @@ -132,3 +132,36 @@ def color_pair(color_fg, color_bg): return "{},{}".format(color_fg, color_bg) else: return color_fg + + +def text_block(text, margin=0): + """ + Pad block of text with whitespace to form a regular block, optionally + adding a margin. + """ + + # add vertical margin + vertical_margin = margin // 2 + text = "{}{}{}".format( + "\n" * vertical_margin, + text, + "\n" * vertical_margin + ) + + lines = text.split("\n") + longest_len = max(len(l) for l in lines) + margin + + # pad block and add horizontal margin + text = "\n".join( + "{pre}{line}{post}".format( + pre=" " * margin, + line=l, + post=" " * (longest_len - len(l))) + for l in lines) + + return text + + +def colored_text_block(text, margin=0, color_pair=""): + """ Like text_block, but also colors it.""" + return string_color_and_reset(text_block(text, margin=margin), color_pair) From 26f7dae9908a008749b1e5b9a765a82140e5a108 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Wed, 9 Jan 2019 14:14:16 +0100 Subject: [PATCH 6/7] Make code block margin configurable. --- matrix/colors.py | 10 ++++++---- matrix/config.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/matrix/colors.py b/matrix/colors.py index 218cfe2..2f51936 100644 --- a/matrix/colors.py +++ b/matrix/colors.py @@ -310,6 +310,8 @@ class Formatted(object): G.CONFIG.color.untagged_code_bg ) + margin = G.CONFIG.look.code_block_margin + if attributes["preformatted"]: # code block @@ -318,7 +320,7 @@ class Formatted(object): except ClassNotFound: return colored_text_block( string, - margin=2, + margin=margin, color_pair=code_color_pair) try: @@ -326,10 +328,10 @@ class Formatted(object): except ClassNotFound: style = "native" - code_block = text_block(string, margin=2) + code_block = text_block(string, margin=margin) - # highlight adds a newline to the end of the string, remove it - # from the output + # highlight adds a newline to the end of the string, remove + # it from the output highlighted_code = highlight( code_block, lexer, diff --git a/matrix/config.py b/matrix/config.py index 98f51c2..d16d456 100644 --- a/matrix/config.py +++ b/matrix/config.py @@ -496,6 +496,16 @@ class MatrixConfig(WeechatConfig): "native", "Pygments style to use for highlighting source code blocks", ), + Option( + "code_block_margin", + "integer", + "", + 0, + 100, + "2", + ("Number of spaces to add as a margin around around a code " + "block"), + ), ] network_options = [ From dcc5a3e5304aebad0b61873ecafefc6f410a6954 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Wed, 9 Jan 2019 14:44:37 +0100 Subject: [PATCH 7/7] Make displaying preformatted code as blocks configurable. --- matrix/colors.py | 17 ++++++++++++----- matrix/config.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/matrix/colors.py b/matrix/colors.py index 2f51936..3eeabb8 100644 --- a/matrix/colors.py +++ b/matrix/colors.py @@ -318,17 +318,24 @@ class Formatted(object): try: lexer = get_lexer_by_name(value) except ClassNotFound: - return colored_text_block( - string, - margin=margin, - color_pair=code_color_pair) + if G.CONFIG.look.code_blocks: + return colored_text_block( + string, + margin=margin, + color_pair=code_color_pair) + else: + return string_color_and_reset(string, + code_color_pair) try: style = get_style_by_name(G.CONFIG.look.pygments_style) except ClassNotFound: style = "native" - code_block = text_block(string, margin=margin) + if G.CONFIG.look.code_blocks: + code_block = text_block(string, margin=margin) + else: + code_block = string # highlight adds a newline to the end of the string, remove # it from the output diff --git a/matrix/config.py b/matrix/config.py index d16d456..4f28963 100644 --- a/matrix/config.py +++ b/matrix/config.py @@ -496,6 +496,17 @@ class MatrixConfig(WeechatConfig): "native", "Pygments style to use for highlighting source code blocks", ), + Option( + "code_blocks", + "boolean", + "", + 0, + 0, + "on", + ("Display preformatted code blocks as rectangular areas by " + "padding them with whitespace up to the length of the longest " + "line (with optional margin)"), + ), Option( "code_block_margin", "integer",