diff --git a/matrix/__init__.py b/matrix/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/matrix/colors.py b/matrix/colors.py new file mode 100644 index 0000000..6e72b85 --- /dev/null +++ b/matrix/colors.py @@ -0,0 +1,845 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from collections import namedtuple + +import webcolors + +try: + from HTMLParser import HTMLParser +except ImportError: + from html.parser import HTMLParser + + +FormattedString = namedtuple( + 'FormattedString', + ['text', 'attributes'] +) + + +# TODO this should be a typed dict. +DEFAULT_ATRIBUTES = { + "bold": False, + "italic": False, + "underline": False, + "strikethrough": False, + "quote": False, + "fgcolor": None, + "bgcolor": None +} + + +class MatrixHtmlParser(HTMLParser): + # TODO bg color + # TODO bullets + def __init__(self): + HTMLParser.__init__(self) + self.text = "" # type: str + self.substrings = [] # type: List[FormattedString] + self.attributes = DEFAULT_ATRIBUTES.copy() + + def _toggle_attribute(self, attribute): + if self.text: + self.substrings.append( + FormattedString(self.text, self.attributes.copy()) + ) + self.text = "" + self.attributes[attribute] = not self.attributes[attribute] + + def handle_starttag(self, tag, attrs): + if tag == "strong": + self._toggle_attribute("bold") + elif tag == "em": + self._toggle_attribute("italic") + elif tag == "u": + self._toggle_attribute("underline") + elif tag == "del": + self._toggle_attribute("strikethrough") + elif tag == "blockquote": + self._toggle_attribute("quote") + elif tag == "blockquote": + self._toggle_attribute("quote") + elif tag == "font": + for key, value in attrs: + if key == "color": + color = color_html_to_weechat(value) + + if not color: + continue + + if self.text: + self.substrings.append( + FormattedString(self.text, self.attributes.copy()) + ) + self.text = "" + self.attributes["fgcolor"] = color + else: + # W.prnt("", "Unhandled tag {t}".format(t=tag)) + pass + + def handle_endtag(self, tag): + if tag == "strong": + self._toggle_attribute("bold") + elif tag == "em": + self._toggle_attribute("italic") + elif tag == "u": + self._toggle_attribute("underline") + elif tag == "del": + self._toggle_attribute("strikethrough") + elif tag == "blockquote": + self._toggle_attribute("quote") + elif tag == "font": + if self.text: + self.substrings.append( + FormattedString(self.text, self.attributes.copy()) + ) + self.text = "" + self.attributes["fgcolor"] = None + else: + pass + + def handle_data(self, data): + self.text = self.text + data + + def get_substrings(self): + if self.text: + self.substrings.append( + FormattedString(self.text, self.attributes.copy()) + ) + + return self.substrings + + +def color_line_to_weechat(color_string): + # type: (str) -> str + line_colors = { + "0": "white", + "1": "black", + "2": "blue", + "3": "green", + "4": "lightred", + "5": "red", + "6": "magenta", + "7": "brown", + "8": "yellow", + "9": "lightgreen", + "10": "cyan", + "11": "lightcyan", + "12": "lightblue", + "13": "lightmagenta", + "14": "darkgray", + "15": "gray", + "16": "52", + "17": "94", + "18": "100", + "19": "58", + "20": "22", + "21": "29", + "22": "23", + "23": "24", + "24": "17", + "25": "54", + "26": "53", + "27": "89", + "28": "88", + "29": "130", + "30": "142", + "31": "64", + "32": "28", + "33": "35", + "34": "30", + "35": "25", + "36": "18", + "37": "91", + "38": "90", + "39": "125", + "40": "124", + "41": "166", + "42": "184", + "43": "106", + "44": "34", + "45": "49", + "46": "37", + "47": "33", + "48": "19", + "49": "129", + "50": "127", + "51": "161", + "52": "196", + "53": "208", + "54": "226", + "55": "154", + "56": "46", + "57": "86", + "58": "51", + "59": "75", + "60": "21", + "61": "171", + "62": "201", + "63": "198", + "64": "203", + "65": "215", + "66": "227", + "67": "191", + "68": "83", + "69": "122", + "70": "87", + "71": "111", + "72": "63", + "73": "177", + "74": "207", + "75": "205", + "76": "217", + "77": "223", + "78": "229", + "79": "193", + "80": "157", + "81": "158", + "82": "159", + "83": "153", + "84": "147", + "85": "183", + "86": "219", + "87": "212", + "88": "16", + "89": "233", + "90": "235", + "91": "237", + "92": "239", + "93": "241", + "94": "244", + "95": "247", + "96": "250", + "97": "254", + "98": "231", + "99": "default" + } + + assert color_string in line_colors + + return line_colors[color_string] + + +# The functions colour_dist_sq(), colour_to_6cube(), and colour_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): + # 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): + # pylint: disable=invalid-name + # type: (int) -> int + if v < 48: + return 0 + if v < 114: + return 1 + return (v - 35) // 40 + + +def colour_find_rgb(r, g, b): + # type: (int, int, int) -> int + """Convert an RGB triplet to the xterm(1) 256 colour 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 + closest grey, and use the nearest of the two. + + Note that the xterm has much lower resolution for darker colours (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). + """ + # pylint: disable=invalid-name + 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) + + cr = q2c[qr] + cg = q2c[qg] + cb = q2c[qb] + + # If we have hit the colour exactly, return early. + if (cr == r and cg == g and cb == b): + return 16 + (36 * qr) + (6 * qg) + qb + + # Work out the closest grey (average of RGB). + grey_avg = (r + g + b) // 3 + + if grey_avg > 238: + grey_idx = 23 + else: + grey_idx = (grey_avg - 3) // 10 + + grey = 8 + (10 * grey_idx) + + # Is grey or 6x6x6 colour closest? + d = colour_dist_sq(cr, cg, cb, r, g, b) + + if colour_dist_sq(grey, grey, grey, r, g, b) < d: + idx = 232 + grey_idx + else: + idx = 16 + (36 * qr) + (6 * qg) + qb + + return idx + + +def color_html_to_weechat(color): + # type: (str) -> str + first_16 = { + (0, 0, 0): "black", # 0 + (128, 0, 0): "red", # 1 + (0, 128, 0): "green", # 2 + (128, 128, 0): "brown", # 3 + (0, 0, 128): "blue", # 4 + (128, 0, 128): "magenta", # 5 + (0, 128, 128): "cyan", # 6 + (192, 192, 192): "default", # 7 + (128, 128, 128): "gray", # 8 + (255, 0, 0): "lightred", # 9 + (0, 255, 0): "lightgreen", # 11 + (255, 255, 0): "yellow", # 12 + (0, 0, 255): "lightblue", # 13 + (255, 0, 255): "lightmagenta", # 14 + (0, 255, 255): "lightcyan", # 15 + } + + try: + rgb_color = webcolors.html5_parse_legacy_color(color) + except ValueError: + return None + + if rgb_color in first_16: + return first_16[rgb_color] + + return str(colour_find_rgb(*rgb_color)) + + +def color_weechat_to_html(color): + first_16 = { + "black": "black", # 0 + "red": "maroon", # 1 + "green": "green", # 2 + "brown": "olive", # 3 + "blue": "navy", # 4 + "magenta": "purple", # 5 + "cyan": "teal", # 6 + "default": "silver", # 7 + "gray": "grey", # 8 + "lightred": "red", # 9 + "lightgreen": "lime", # 11 + "yellow": "yellow", # 12 + "lightblue": "fuchsia", # 13 + "lightmagenta": "aqua", # 14 + "lightcyan": "white", # 15 + } + + hex_colors = { + "0": "#000000", + "1": "#800000", + "2": "#008000", + "3": "#808000", + "4": "#000080", + "5": "#800080", + "6": "#008080", + "7": "#c0c0c0", + "8": "#808080", + "9": "#ff0000", + "10": "#00ff00", + "11": "#ffff00", + "12": "#0000ff", + "13": "#ff00ff", + "14": "#00ffff", + "15": "#ffffff", + "16": "#000000", + "17": "#00005f", + "18": "#000087", + "19": "#0000af", + "20": "#0000d7", + "21": "#0000ff", + "22": "#005f00", + "23": "#005f5f", + "24": "#005f87", + "25": "#005faf", + "26": "#005fd7", + "27": "#005fff", + "28": "#008700", + "29": "#00875f", + "30": "#008787", + "31": "#0087af", + "32": "#0087d7", + "33": "#0087ff", + "34": "#00af00", + "35": "#00af5f", + "36": "#00af87", + "37": "#00afaf", + "38": "#00afd7", + "39": "#00afff", + "40": "#00d700", + "41": "#00d75f", + "42": "#00d787", + "43": "#00d7af", + "44": "#00d7d7", + "45": "#00d7ff", + "46": "#00ff00", + "47": "#00ff5f", + "48": "#00ff87", + "49": "#00ffaf", + "50": "#00ffd7", + "51": "#00ffff", + "52": "#5f0000", + "53": "#5f005f", + "54": "#5f0087", + "55": "#5f00af", + "56": "#5f00d7", + "57": "#5f00ff", + "58": "#5f5f00", + "59": "#5f5f5f", + "60": "#5f5f87", + "61": "#5f5faf", + "62": "#5f5fd7", + "63": "#5f5fff", + "64": "#5f8700", + "65": "#5f875f", + "66": "#5f8787", + "67": "#5f87af", + "68": "#5f87d7", + "69": "#5f87ff", + "70": "#5faf00", + "71": "#5faf5f", + "72": "#5faf87", + "73": "#5fafaf", + "74": "#5fafd7", + "75": "#5fafff", + "76": "#5fd700", + "77": "#5fd75f", + "78": "#5fd787", + "79": "#5fd7af", + "80": "#5fd7d7", + "81": "#5fd7ff", + "82": "#5fff00", + "83": "#5fff5f", + "84": "#5fff87", + "85": "#5fffaf", + "86": "#5fffd7", + "87": "#5fffff", + "88": "#870000", + "89": "#87005f", + "90": "#870087", + "91": "#8700af", + "92": "#8700d7", + "93": "#8700ff", + "94": "#875f00", + "95": "#875f5f", + "96": "#875f87", + "97": "#875faf", + "98": "#875fd7", + "99": "#875fff", + "100": "#878700", + "101": "#87875f", + "102": "#878787", + "103": "#8787af", + "104": "#8787d7", + "105": "#8787ff", + "106": "#87af00", + "107": "#87af5f", + "108": "#87af87", + "109": "#87afaf", + "110": "#87afd7", + "111": "#87afff", + "112": "#87d700", + "113": "#87d75f", + "114": "#87d787", + "115": "#87d7af", + "116": "#87d7d7", + "117": "#87d7ff", + "118": "#87ff00", + "119": "#87ff5f", + "120": "#87ff87", + "121": "#87ffaf", + "122": "#87ffd7", + "123": "#87ffff", + "124": "#af0000", + "125": "#af005f", + "126": "#af0087", + "127": "#af00af", + "128": "#af00d7", + "129": "#af00ff", + "130": "#af5f00", + "131": "#af5f5f", + "132": "#af5f87", + "133": "#af5faf", + "134": "#af5fd7", + "135": "#af5fff", + "136": "#af8700", + "137": "#af875f", + "138": "#af8787", + "139": "#af87af", + "140": "#af87d7", + "141": "#af87ff", + "142": "#afaf00", + "143": "#afaf5f", + "144": "#afaf87", + "145": "#afafaf", + "146": "#afafd7", + "147": "#afafff", + "148": "#afd700", + "149": "#afd75f", + "150": "#afd787", + "151": "#afd7af", + "152": "#afd7d7", + "153": "#afd7ff", + "154": "#afff00", + "155": "#afff5f", + "156": "#afff87", + "157": "#afffaf", + "158": "#afffd7", + "159": "#afffff", + "160": "#d70000", + "161": "#d7005f", + "162": "#d70087", + "163": "#d700af", + "164": "#d700d7", + "165": "#d700ff", + "166": "#d75f00", + "167": "#d75f5f", + "168": "#d75f87", + "169": "#d75faf", + "170": "#d75fd7", + "171": "#d75fff", + "172": "#d78700", + "173": "#d7875f", + "174": "#d78787", + "175": "#d787af", + "176": "#d787d7", + "177": "#d787ff", + "178": "#d7af00", + "179": "#d7af5f", + "180": "#d7af87", + "181": "#d7afaf", + "182": "#d7afd7", + "183": "#d7afff", + "184": "#d7d700", + "185": "#d7d75f", + "186": "#d7d787", + "187": "#d7d7af", + "188": "#d7d7d7", + "189": "#d7d7ff", + "190": "#d7ff00", + "191": "#d7ff5f", + "192": "#d7ff87", + "193": "#d7ffaf", + "194": "#d7ffd7", + "195": "#d7ffff", + "196": "#ff0000", + "197": "#ff005f", + "198": "#ff0087", + "199": "#ff00af", + "200": "#ff00d7", + "201": "#ff00ff", + "202": "#ff5f00", + "203": "#ff5f5f", + "204": "#ff5f87", + "205": "#ff5faf", + "206": "#ff5fd7", + "207": "#ff5fff", + "208": "#ff8700", + "209": "#ff875f", + "210": "#ff8787", + "211": "#ff87af", + "212": "#ff87d7", + "213": "#ff87ff", + "214": "#ffaf00", + "215": "#ffaf5f", + "216": "#ffaf87", + "217": "#ffafaf", + "218": "#ffafd7", + "219": "#ffafff", + "220": "#ffd700", + "221": "#ffd75f", + "222": "#ffd787", + "223": "#ffd7af", + "224": "#ffd7d7", + "225": "#ffd7ff", + "226": "#ffff00", + "227": "#ffff5f", + "228": "#ffff87", + "229": "#ffffaf", + "230": "#ffffd7", + "231": "#ffffff", + "232": "#080808", + "233": "#121212", + "234": "#1c1c1c", + "235": "#262626", + "236": "#303030", + "237": "#3a3a3a", + "238": "#444444", + "239": "#4e4e4e", + "240": "#585858", + "241": "#626262", + "242": "#6c6c6c", + "243": "#767676", + "244": "#808080", + "245": "#8a8a8a", + "246": "#949494", + "247": "#9e9e9e", + "248": "#a8a8a8", + "249": "#b2b2b2", + "250": "#bcbcbc", + "251": "#c6c6c6", + "252": "#d0d0d0", + "253": "#dadada", + "254": "#e4e4e4", + "255": "#eeeeee" + } + + if color in first_16: + return first_16[color] + + hex_color = hex_colors[color] + + try: + return webcolors.hex_to_name(hex_color) + except ValueError: + return hex_color + + +# TODO reverse video +def parse_input_line(line): + # type: (str) -> List[FormattedString] + """Parses the weechat input line and produces formatted strings that can be + later converted to HTML or to a string for weechat's print functions + """ + text = "" # type: str + substrings = [] # type: List[FormattedString] + attributes = DEFAULT_ATRIBUTES.copy() + + i = 0 + while i < len(line): + # Bold + if line[i] == "\x02": + if text: + substrings.append(FormattedString(text, attributes.copy())) + text = "" + attributes["bold"] = not attributes["bold"] + i = i + 1 + + # Color + elif line[i] == "\x03": + if text: + substrings.append(FormattedString(text, attributes.copy())) + text = "" + i = i + 1 + + # check if it's a valid color, add it to the attributes + if line[i].isdigit(): + color_string = line[i] + i = i + 1 + + if line[i].isdigit(): + if color_string == "0": + color_string = line[i] + else: + color_string = color_string + line[i] + i = i + 1 + + attributes["fgcolor"] = color_line_to_weechat(color_string) + else: + attributes["fgcolor"] = None + + # check if we have a background color + if line[i] == "," and line[i+1].isdigit(): + color_string = line[i+1] + i = i + 2 + + if line[i].isdigit(): + if color_string == "0": + color_string = line[i] + else: + color_string = color_string + line[i] + i = i + 1 + + attributes["bgcolor"] = color_line_to_weechat(color_string) + else: + attributes["bgcolor"] = None + # Reset + elif line[i] == "\x0F": + if text: + substrings.append(FormattedString(text, attributes.copy())) + text = "" + # Reset all the attributes + attributes = DEFAULT_ATRIBUTES.copy() + i = i + 1 + # Italic + elif line[i] == "\x1D": + if text: + substrings.append(FormattedString(text, attributes.copy())) + text = "" + attributes["italic"] = not attributes["italic"] + i = i + 1 + + # Underline + elif line[i] == "\x1F": + if text: + substrings.append(FormattedString(text, attributes.copy())) + text = "" + attributes["underline"] = not attributes["underline"] + i = i + 1 + + # Normal text + else: + text = text + line[i] + i = i + 1 + + substrings.append(FormattedString(text, attributes)) + return substrings + + +def formatted(strings): + for string in strings: + if string.attributes != DEFAULT_ATRIBUTES: + return True + return False + + +def formatted_to_html(strings): + # type: (List[FormattedString]) -> str + # TODO BG COLOR + def add_attribute(string, name, value): + if name == "bold" and value: + return "{bold_on}{text}{bold_off}".format( + bold_on="", + text=string, + bold_off="") + elif name == "italic" and value: + return "{italic_on}{text}{italic_off}".format( + italic_on="", + text=string, + italic_off="") + elif name == "underline" and value: + return "{underline_on}{text}{underline_off}".format( + underline_on="", + text=string, + underline_off="") + elif name == "strikethrough" and value: + return "{strike_on}{text}{strike_off}".format( + strike_on="", + text=string, + strike_off="") + elif name == "quote" and value: + return "{quote_on}{text}{quote_off}".format( + quote_on="
", + text=string, + quote_off="
") + elif name == "fgcolor" and value: + return "{color_on}{text}{color_off}".format( + color_on="".format( + color=color_weechat_to_html(value) + ), + text=string, + color_off="") + + return string + + def format_string(formatted_string): + text = formatted_string.text + attributes = formatted_string.attributes + + for key, value in attributes.items(): + text = add_attribute(text, key, value) + return text + + html_string = map(format_string, strings) + return "".join(html_string) + + +# TODO do we want at least some formating using unicode +# (strikethrough, quotes)? +def formatted_to_plain(strings): + # type: (List[FormattedString]) -> str + def strip_atribute(string, _, __): + return string + + def format_string(formatted_string): + text = formatted_string.text + attributes = formatted_string.attributes + + for key, value in attributes.items(): + text = strip_atribute(text, key, value) + return text + + plain_string = map(format_string, strings) + return "".join(plain_string) + + +def html_to_formatted(html): + parser = MatrixHtmlParser() + parser.feed(html) + return parser.get_substrings() + + +def string_strikethrough(string): + return "".join(["{}\u0336".format(c) for c in string]) + + +def formatted_to_weechat(W, strings): + # type: (weechat, List[colors.FormattedString]) -> str + # TODO BG COLOR + def add_attribute(string, name, value): + if name == "bold" and value: + return "{bold_on}{text}{bold_off}".format( + bold_on=W.color("bold"), + text=string, + bold_off=W.color("-bold")) + + elif name == "italic" and value: + return "{italic_on}{text}{italic_off}".format( + italic_on=W.color("italic"), + text=string, + italic_off=W.color("-italic")) + + elif name == "underline" and value: + return "{underline_on}{text}{underline_off}".format( + underline_on=W.color("underline"), + text=string, + underline_off=W.color("-underline")) + + elif name == "strikethrough" and value: + return string_strikethrough(string) + + elif name == "quote" and value: + return "“{text}”".format(text=string) + + elif name == "fgcolor" and value: + return "{color_on}{text}{color_off}".format( + color_on=W.color(value), + text=string, + color_off=W.color("resetcolor")) + + elif name == "bgcolor" and value: + return "{color_on}{text}{color_off}".format( + color_on=W.color("," + value), + text=string, + color_off=W.color("resetcolor")) + + return string + + def format_string(formatted_string): + text = formatted_string.text + attributes = formatted_string.attributes + + for key, value in attributes.items(): + text = add_attribute(text, key, value) + return text + + weechat_strings = map(format_string, strings) + return "".join(weechat_strings) diff --git a/weechat-matrix.py b/weechat-matrix.py index a296a52..1d47216 100644 --- a/weechat-matrix.py +++ b/weechat-matrix.py @@ -22,13 +22,8 @@ from functools import wraps # pylint: disable=unused-import from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any) -import webcolors from http_parser.pyparser import HttpParser - -try: - from HTMLParser import HTMLParser -except ImportError: - from html.parser import HTMLParser +from matrix import colors # pylint: disable=import-error @@ -275,14 +270,14 @@ class MatrixMessage: ): # type: (...) -> None # pylint: disable=dangerous-default-value - self.type = message_type # MessageType - self.request = None # HttpRequest - self.response = None # HttpResponse - self.extra_data = extra_data # Dict[str, Any] + self.type = message_type # type: MessageType + self.request = None # type: HttpRequest + self.response = None # type: HttpResponse + self.extra_data = extra_data # type: Dict[str, Any] - self.creation_time = time.time() - self.send_time = None - self.receive_time = None + self.creation_time = time.time() # type: float + self.send_time = None # type: float + self.receive_time = None # type: float if message_type == MessageType.LOGIN: path = ("{api}/login").format(api=MATRIX_API_PATH) @@ -596,833 +591,6 @@ class MatrixServer: "", "server_config_change_cb", self.name, "", "") -FormattedString = namedtuple( - 'FormattedString', - ['text', 'attributes'] -) - -DEFAULT_ATRIBUTES = { - "bold": False, - "italic": False, - "underline": False, - "strikethrough": False, - "quote": False, - "fgcolor": None, - "bgcolor": None -} - - -def color_line_to_weechat(color_string): - # type: (str) -> str - line_colors = { - "0": "white", - "1": "black", - "2": "blue", - "3": "green", - "4": "lightred", - "5": "red", - "6": "magenta", - "7": "brown", - "8": "yellow", - "9": "lightgreen", - "10": "cyan", - "11": "lightcyan", - "12": "lightblue", - "13": "lightmagenta", - "14": "darkgray", - "15": "gray", - "16": "52", - "17": "94", - "18": "100", - "19": "58", - "20": "22", - "21": "29", - "22": "23", - "23": "24", - "24": "17", - "25": "54", - "26": "53", - "27": "89", - "28": "88", - "29": "130", - "30": "142", - "31": "64", - "32": "28", - "33": "35", - "34": "30", - "35": "25", - "36": "18", - "37": "91", - "38": "90", - "39": "125", - "40": "124", - "41": "166", - "42": "184", - "43": "106", - "44": "34", - "45": "49", - "46": "37", - "47": "33", - "48": "19", - "49": "129", - "50": "127", - "51": "161", - "52": "196", - "53": "208", - "54": "226", - "55": "154", - "56": "46", - "57": "86", - "58": "51", - "59": "75", - "60": "21", - "61": "171", - "62": "201", - "63": "198", - "64": "203", - "65": "215", - "66": "227", - "67": "191", - "68": "83", - "69": "122", - "70": "87", - "71": "111", - "72": "63", - "73": "177", - "74": "207", - "75": "205", - "76": "217", - "77": "223", - "78": "229", - "79": "193", - "80": "157", - "81": "158", - "82": "159", - "83": "153", - "84": "147", - "85": "183", - "86": "219", - "87": "212", - "88": "16", - "89": "233", - "90": "235", - "91": "237", - "92": "239", - "93": "241", - "94": "244", - "95": "247", - "96": "250", - "97": "254", - "98": "231", - "99": "default" - } - - assert color_string in line_colors - - return line_colors[color_string] - - -# The functions colour_dist_sq(), colour_to_6cube(), and colour_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): - # 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): - # pylint: disable=invalid-name - # type: (int) -> int - if v < 48: - return 0 - if v < 114: - return 1 - return (v - 35) // 40 - - -def colour_find_rgb(r, g, b): - """Convert an RGB triplet to the xterm(1) 256 colour 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 - closest grey, and use the nearest of the two. - - Note that the xterm has much lower resolution for darker colours (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). - """ - # pylint: disable=invalid-name - # type: (int, int, int) -> int - 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) - - cr = q2c[qr] - cg = q2c[qg] - cb = q2c[qb] - - # If we have hit the colour exactly, return early. - if (cr == r and cg == g and cb == b): - return 16 + (36 * qr) + (6 * qg) + qb - - # Work out the closest grey (average of RGB). - grey_avg = (r + g + b) // 3 - - if grey_avg > 238: - grey_idx = 23 - else: - grey_idx = (grey_avg - 3) // 10 - - grey = 8 + (10 * grey_idx) - - # Is grey or 6x6x6 colour closest? - d = colour_dist_sq(cr, cg, cb, r, g, b) - - if colour_dist_sq(grey, grey, grey, r, g, b) < d: - idx = 232 + grey_idx - else: - idx = 16 + (36 * qr) + (6 * qg) + qb - - return idx - - -def color_html_to_weechat(color): - # type: (str) -> str - first_16 = { - (0, 0, 0): "black", # 0 - (128, 0, 0): "red", # 1 - (0, 128, 0): "green", # 2 - (128, 128, 0): "brown", # 3 - (0, 0, 128): "blue", # 4 - (128, 0, 128): "magenta", # 5 - (0, 128, 128): "cyan", # 6 - (192, 192, 192): "default", # 7 - (128, 128, 128): "gray", # 8 - (255, 0, 0): "lightred", # 9 - (0, 255, 0): "lightgreen", # 11 - (255, 255, 0): "yellow", # 12 - (0, 0, 255): "lightblue", # 13 - (255, 0, 255): "lightmagenta", # 14 - (0, 255, 255): "lightcyan", # 15 - } - - try: - rgb_color = webcolors.html5_parse_legacy_color(color) - except ValueError: - return None - - if rgb_color in first_16: - return first_16[rgb_color] - - return str(colour_find_rgb(*rgb_color)) - - -def color_weechat_to_html(color): - first_16 = { - "black": "black", # 0 - "red": "maroon", # 1 - "green": "green", # 2 - "brown": "olive", # 3 - "blue": "navy", # 4 - "magenta": "purple", # 5 - "cyan": "teal", # 6 - "default": "silver", # 7 - "gray": "grey", # 8 - "lightred": "red", # 9 - "lightgreen": "lime", # 11 - "yellow": "yellow", # 12 - "lightblue": "fuchsia", # 13 - "lightmagenta": "aqua", # 14 - "lightcyan": "white", # 15 - } - - hex_colors = { - "0": "#000000", - "1": "#800000", - "2": "#008000", - "3": "#808000", - "4": "#000080", - "5": "#800080", - "6": "#008080", - "7": "#c0c0c0", - "8": "#808080", - "9": "#ff0000", - "10": "#00ff00", - "11": "#ffff00", - "12": "#0000ff", - "13": "#ff00ff", - "14": "#00ffff", - "15": "#ffffff", - "16": "#000000", - "17": "#00005f", - "18": "#000087", - "19": "#0000af", - "20": "#0000d7", - "21": "#0000ff", - "22": "#005f00", - "23": "#005f5f", - "24": "#005f87", - "25": "#005faf", - "26": "#005fd7", - "27": "#005fff", - "28": "#008700", - "29": "#00875f", - "30": "#008787", - "31": "#0087af", - "32": "#0087d7", - "33": "#0087ff", - "34": "#00af00", - "35": "#00af5f", - "36": "#00af87", - "37": "#00afaf", - "38": "#00afd7", - "39": "#00afff", - "40": "#00d700", - "41": "#00d75f", - "42": "#00d787", - "43": "#00d7af", - "44": "#00d7d7", - "45": "#00d7ff", - "46": "#00ff00", - "47": "#00ff5f", - "48": "#00ff87", - "49": "#00ffaf", - "50": "#00ffd7", - "51": "#00ffff", - "52": "#5f0000", - "53": "#5f005f", - "54": "#5f0087", - "55": "#5f00af", - "56": "#5f00d7", - "57": "#5f00ff", - "58": "#5f5f00", - "59": "#5f5f5f", - "60": "#5f5f87", - "61": "#5f5faf", - "62": "#5f5fd7", - "63": "#5f5fff", - "64": "#5f8700", - "65": "#5f875f", - "66": "#5f8787", - "67": "#5f87af", - "68": "#5f87d7", - "69": "#5f87ff", - "70": "#5faf00", - "71": "#5faf5f", - "72": "#5faf87", - "73": "#5fafaf", - "74": "#5fafd7", - "75": "#5fafff", - "76": "#5fd700", - "77": "#5fd75f", - "78": "#5fd787", - "79": "#5fd7af", - "80": "#5fd7d7", - "81": "#5fd7ff", - "82": "#5fff00", - "83": "#5fff5f", - "84": "#5fff87", - "85": "#5fffaf", - "86": "#5fffd7", - "87": "#5fffff", - "88": "#870000", - "89": "#87005f", - "90": "#870087", - "91": "#8700af", - "92": "#8700d7", - "93": "#8700ff", - "94": "#875f00", - "95": "#875f5f", - "96": "#875f87", - "97": "#875faf", - "98": "#875fd7", - "99": "#875fff", - "100": "#878700", - "101": "#87875f", - "102": "#878787", - "103": "#8787af", - "104": "#8787d7", - "105": "#8787ff", - "106": "#87af00", - "107": "#87af5f", - "108": "#87af87", - "109": "#87afaf", - "110": "#87afd7", - "111": "#87afff", - "112": "#87d700", - "113": "#87d75f", - "114": "#87d787", - "115": "#87d7af", - "116": "#87d7d7", - "117": "#87d7ff", - "118": "#87ff00", - "119": "#87ff5f", - "120": "#87ff87", - "121": "#87ffaf", - "122": "#87ffd7", - "123": "#87ffff", - "124": "#af0000", - "125": "#af005f", - "126": "#af0087", - "127": "#af00af", - "128": "#af00d7", - "129": "#af00ff", - "130": "#af5f00", - "131": "#af5f5f", - "132": "#af5f87", - "133": "#af5faf", - "134": "#af5fd7", - "135": "#af5fff", - "136": "#af8700", - "137": "#af875f", - "138": "#af8787", - "139": "#af87af", - "140": "#af87d7", - "141": "#af87ff", - "142": "#afaf00", - "143": "#afaf5f", - "144": "#afaf87", - "145": "#afafaf", - "146": "#afafd7", - "147": "#afafff", - "148": "#afd700", - "149": "#afd75f", - "150": "#afd787", - "151": "#afd7af", - "152": "#afd7d7", - "153": "#afd7ff", - "154": "#afff00", - "155": "#afff5f", - "156": "#afff87", - "157": "#afffaf", - "158": "#afffd7", - "159": "#afffff", - "160": "#d70000", - "161": "#d7005f", - "162": "#d70087", - "163": "#d700af", - "164": "#d700d7", - "165": "#d700ff", - "166": "#d75f00", - "167": "#d75f5f", - "168": "#d75f87", - "169": "#d75faf", - "170": "#d75fd7", - "171": "#d75fff", - "172": "#d78700", - "173": "#d7875f", - "174": "#d78787", - "175": "#d787af", - "176": "#d787d7", - "177": "#d787ff", - "178": "#d7af00", - "179": "#d7af5f", - "180": "#d7af87", - "181": "#d7afaf", - "182": "#d7afd7", - "183": "#d7afff", - "184": "#d7d700", - "185": "#d7d75f", - "186": "#d7d787", - "187": "#d7d7af", - "188": "#d7d7d7", - "189": "#d7d7ff", - "190": "#d7ff00", - "191": "#d7ff5f", - "192": "#d7ff87", - "193": "#d7ffaf", - "194": "#d7ffd7", - "195": "#d7ffff", - "196": "#ff0000", - "197": "#ff005f", - "198": "#ff0087", - "199": "#ff00af", - "200": "#ff00d7", - "201": "#ff00ff", - "202": "#ff5f00", - "203": "#ff5f5f", - "204": "#ff5f87", - "205": "#ff5faf", - "206": "#ff5fd7", - "207": "#ff5fff", - "208": "#ff8700", - "209": "#ff875f", - "210": "#ff8787", - "211": "#ff87af", - "212": "#ff87d7", - "213": "#ff87ff", - "214": "#ffaf00", - "215": "#ffaf5f", - "216": "#ffaf87", - "217": "#ffafaf", - "218": "#ffafd7", - "219": "#ffafff", - "220": "#ffd700", - "221": "#ffd75f", - "222": "#ffd787", - "223": "#ffd7af", - "224": "#ffd7d7", - "225": "#ffd7ff", - "226": "#ffff00", - "227": "#ffff5f", - "228": "#ffff87", - "229": "#ffffaf", - "230": "#ffffd7", - "231": "#ffffff", - "232": "#080808", - "233": "#121212", - "234": "#1c1c1c", - "235": "#262626", - "236": "#303030", - "237": "#3a3a3a", - "238": "#444444", - "239": "#4e4e4e", - "240": "#585858", - "241": "#626262", - "242": "#6c6c6c", - "243": "#767676", - "244": "#808080", - "245": "#8a8a8a", - "246": "#949494", - "247": "#9e9e9e", - "248": "#a8a8a8", - "249": "#b2b2b2", - "250": "#bcbcbc", - "251": "#c6c6c6", - "252": "#d0d0d0", - "253": "#dadada", - "254": "#e4e4e4", - "255": "#eeeeee" - } - - if color in first_16: - return first_16[color] - - hex_color = hex_colors[color] - - try: - return webcolors.hex_to_name(hex_color) - except ValueError: - return hex_color - - -# TODO reverse video -def parse_input_line(line): - """Parses the weechat input line and produces formatted strings that can be - later converted to HTML or to a string for weechat's print functions - """ - # type: (str) -> List[FormattedString] - text = "" # type: str - substrings = [] # type: List[FormattedString] - attributes = DEFAULT_ATRIBUTES.copy() - - i = 0 - while i < len(line): - # Bold - if line[i] == "\x02": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - attributes["bold"] = not attributes["bold"] - i = i + 1 - - # Color - elif line[i] == "\x03": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - i = i + 1 - - # check if it's a valid color, add it to the attributes - if line[i].isdigit(): - color_string = line[i] - i = i + 1 - - if line[i].isdigit(): - if color_string == "0": - color_string = line[i] - else: - color_string = color_string + line[i] - i = i + 1 - - attributes["fgcolor"] = color_line_to_weechat(color_string) - else: - attributes["fgcolor"] = None - - # check if we have a background color - if line[i] == "," and line[i+1].isdigit(): - color_string = line[i+1] - i = i + 2 - - if line[i].isdigit(): - if color_string == "0": - color_string = line[i] - else: - color_string = color_string + line[i] - i = i + 1 - - attributes["bgcolor"] = color_line_to_weechat(color_string) - else: - attributes["bgcolor"] = None - # Reset - elif line[i] == "\x0F": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - # Reset all the attributes - attributes = DEFAULT_ATRIBUTES.copy() - i = i + 1 - # Italic - elif line[i] == "\x1D": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - attributes["italic"] = not attributes["italic"] - i = i + 1 - - # Underline - elif line[i] == "\x1F": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - attributes["underline"] = not attributes["underline"] - i = i + 1 - - # Normal text - else: - text = text + line[i] - i = i + 1 - - substrings.append(FormattedString(text, attributes)) - return substrings - - -def formatted(strings): - for string in strings: - if string.attributes != DEFAULT_ATRIBUTES: - return True - return False - - -def formatted_to_weechat(strings): - # type: (List[FormattedString]) -> str - # TODO BG COLOR - def add_attribute(string, name, value): - if name == "bold" and value: - return "{bold_on}{text}{bold_off}".format( - bold_on=W.color("bold"), - text=string, - bold_off=W.color("-bold")) - - elif name == "italic" and value: - return "{italic_on}{text}{italic_off}".format( - italic_on=W.color("italic"), - text=string, - italic_off=W.color("-italic")) - - elif name == "underline" and value: - return "{underline_on}{text}{underline_off}".format( - underline_on=W.color("underline"), - text=string, - underline_off=W.color("-underline")) - - elif name == "strikethrough" and value: - return string_strikethrough(string) - - elif name == "quote" and value: - return "“{text}”".format(text=string) - - elif name == "fgcolor" and value: - return "{color_on}{text}{color_off}".format( - color_on=W.color(value), - text=string, - color_off=W.color("resetcolor")) - - elif name == "bgcolor" and value: - return "{color_on}{text}{color_off}".format( - color_on=W.color("," + value), - text=string, - color_off=W.color("resetcolor")) - - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - for key, value in attributes.items(): - text = add_attribute(text, key, value) - return text - - weechat_strings = map(format_string, strings) - return "".join(weechat_strings) - - -def formatted_to_html(strings): - # type: (List[FormattedString]) -> str - # TODO BG COLOR - def add_attribute(string, name, value): - if name == "bold" and value: - return "{bold_on}{text}{bold_off}".format( - bold_on="", - text=string, - bold_off="") - elif name == "italic" and value: - return "{italic_on}{text}{italic_off}".format( - italic_on="", - text=string, - italic_off="") - elif name == "underline" and value: - return "{underline_on}{text}{underline_off}".format( - underline_on="", - text=string, - underline_off="") - elif name == "strikethrough" and value: - return "{strike_on}{text}{strike_off}".format( - strike_on="", - text=string, - strike_off="") - elif name == "quote" and value: - return "{quote_on}{text}{quote_off}".format( - quote_on="
", - text=string, - quote_off="
") - elif name == "fgcolor" and value: - return "{color_on}{text}{color_off}".format( - color_on="".format( - color=color_weechat_to_html(value) - ), - text=string, - color_off="") - - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - for key, value in attributes.items(): - text = add_attribute(text, key, value) - return text - - html_string = map(format_string, strings) - return "".join(html_string) - - -# TODO do we want at least some formating using unicode -# (strikethrough, quotes)? -def formatted_to_plain(strings): - # type: (List[FormattedString]) -> str - def strip_atribute(string, _, __): - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - for key, value in attributes.items(): - text = strip_atribute(text, key, value) - return text - - plain_string = map(format_string, strings) - return "".join(plain_string) - - -class MatrixHtmlParser(HTMLParser): - # TODO bg color - # TODO bullets - def __init__(self): - HTMLParser.__init__(self) - self.text = "" # type: str - self.substrings = [] # type: List[FormattedString] - self.attributes = DEFAULT_ATRIBUTES.copy() - - def _toggle_attribute(self, attribute): - if self.text: - self.substrings.append( - FormattedString(self.text, self.attributes.copy()) - ) - self.text = "" - self.attributes[attribute] = not self.attributes[attribute] - - def handle_starttag(self, tag, attrs): - if tag == "strong": - self._toggle_attribute("bold") - elif tag == "em": - self._toggle_attribute("italic") - elif tag == "u": - self._toggle_attribute("underline") - elif tag == "del": - self._toggle_attribute("strikethrough") - elif tag == "blockquote": - self._toggle_attribute("quote") - elif tag == "blockquote": - self._toggle_attribute("quote") - elif tag == "font": - for key, value in attrs: - if key == "color": - color = color_html_to_weechat(value) - - if not color: - continue - - if self.text: - self.substrings.append( - FormattedString(self.text, self.attributes.copy()) - ) - self.text = "" - self.attributes["fgcolor"] = color - else: - # W.prnt("", "Unhandled tag {t}".format(t=tag)) - pass - - def handle_endtag(self, tag): - if tag == "strong": - self._toggle_attribute("bold") - elif tag == "em": - self._toggle_attribute("italic") - elif tag == "u": - self._toggle_attribute("underline") - elif tag == "del": - self._toggle_attribute("strikethrough") - elif tag == "blockquote": - self._toggle_attribute("quote") - elif tag == "font": - if self.text: - self.substrings.append( - FormattedString(self.text, self.attributes.copy()) - ) - self.text = "" - self.attributes["fgcolor"] = None - else: - pass - - def handle_data(self, data): - self.text = self.text + data - - def get_substrings(self): - if self.text: - self.substrings.append( - FormattedString(self.text, self.attributes.copy()) - ) - - return self.substrings - - -def html_to_formatted(html): - parser = MatrixHtmlParser() - parser.feed(html) - return parser.get_substrings() - - def wrap_socket(server, file_descriptor): # type: (MatrixServer, int) -> socket.socket sock = None # type: socket.socket @@ -1501,10 +669,10 @@ def handle_http_response(server, message): reason = ("." if not response or not response["error"] else ": {r}.".format(r=response["error"])) - message = ("{prefix}Login error{reason}").format( + error_message = ("{prefix}Login error{reason}").format( prefix=W.prefix("error"), reason=reason) - server_buffer_prnt(server, message) + server_buffer_prnt(server, error_message) W.unhook(server.timer_hook) server.timer_hook = None @@ -1516,16 +684,16 @@ def handle_http_response(server, message): reason = ("." if not response or not response["error"] else ": {r}.".format(r=response["error"])) - message = ("{prefix}Can't set state{reason}").format( + error_message = ("{prefix}Can't set state{reason}").format( prefix=W.prefix("network"), reason=reason) - server_buffer_prnt(server, message) + server_buffer_prnt(server, error_message) else: - message = ("{prefix}Unhandled 403 error, please inform the " - "developers about this: {error}").format( - prefix=W.prefix("error"), - error=message.response.body) - server_buffer_prnt(server, message) + error_message = ("{prefix}Unhandled 403 error, please inform the " + "developers about this: {error}").format( + prefix=W.prefix("error"), + error=message.response.body) + server_buffer_prnt(server, error_message) else: server_buffer_prnt( @@ -1541,19 +709,19 @@ def handle_http_response(server, message): creation_date = datetime.datetime.fromtimestamp(message.creation_time) done_time = time.time() - message = ("Message of type {t} created at {c}." - "\nMessage lifetime information:" - "\n Send delay: {s} ms" - "\n Receive delay: {r} ms" - "\n Handling time: {h} ms" - "\n Total time: {total} ms").format( - t=message.type, - c=creation_date, - s=(message.send_time - message.creation_time) * 1000, - r=(message.receive_time - message.send_time) * 1000, - h=(done_time - message.receive_time) * 1000, - total=(done_time - message.creation_time) * 1000,) - prnt_debug(DebugType.TIMING, server, message) + info_message = ("Message of type {t} created at {c}." + "\nMessage lifetime information:" + "\n Send delay: {s} ms" + "\n Receive delay: {r} ms" + "\n Handling time: {h} ms" + "\n Total time: {total} ms").format( + t=message.type, + c=creation_date, + s=(message.send_time - message.creation_time) * 1000, + r=(message.receive_time - message.send_time) * 1000, + h=(done_time - message.receive_time) * 1000, + total=(done_time - message.creation_time) * 1000,) + prnt_debug(DebugType.TIMING, server, info_message) return @@ -1709,9 +877,9 @@ def matrix_handle_room_text_message(server, room_id, event, old=False): if 'format' in event['content'] and 'formatted_body' in event['content']: if event['content']['format'] == "org.matrix.custom.html": - formatted_data = html_to_formatted( + formatted_data = colors.html_to_formatted( event['content']['formatted_body']) - msg = formatted_to_weechat(formatted_data) + msg = colors.formatted_to_weechat(W, formatted_data) if event['sender'] in room.users: user = room.users[event['sender']] @@ -2626,17 +1794,20 @@ def room_input_cb(server_name, buffer, input_data): if room.encrypted: return W.WEECHAT_RC_OK - formatted_data = parse_input_line(input_data) + formatted_data = colors.parse_input_line(input_data) - body = {"msgtype": "m.text", "body": formatted_to_plain(formatted_data)} + body = { + "msgtype": "m.text", + "body": colors.formatted_to_plain(formatted_data) + } - if formatted(formatted_data): + if colors.formatted(formatted_data): body["format"] = "org.matrix.custom.html" - body["formatted_body"] = formatted_to_html(formatted_data) + body["formatted_body"] = colors.formatted_to_html(formatted_data) extra_data = { "author": server.user, - "message": formatted_to_weechat(formatted_data), + "message": colors.formatted_to_weechat(W, formatted_data), "room_id": room_id }