From 41127e50abc3e126f953af5ad638f92d0848f9f1 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 22 Sep 2019 17:35:07 +0200 Subject: Move message rendering code to Message.render() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also: - rename format_chars to FORMAT_CHARS because it’s static constant - move Line, Message, and a few funcs/consts to a new poezio.ui module --- poezio/tabs/basetabs.py | 8 ++- poezio/text_buffer.py | 86 +----------------------- poezio/ui/__init__.py | 0 poezio/ui/consts.py | 4 ++ poezio/ui/funcs.py | 60 +++++++++++++++++ poezio/ui/types.py | 158 ++++++++++++++++++++++++++++++++++++++++++++ poezio/windows/base_wins.py | 5 +- poezio/windows/funcs.py | 60 ----------------- poezio/windows/info_wins.py | 2 +- poezio/windows/inputs.py | 11 +-- poezio/windows/text_win.py | 66 +----------------- poezio/xhtml.py | 2 +- 12 files changed, 243 insertions(+), 219 deletions(-) create mode 100644 poezio/ui/__init__.py create mode 100644 poezio/ui/consts.py create mode 100644 poezio/ui/funcs.py create mode 100644 poezio/ui/types.py delete mode 100644 poezio/windows/funcs.py diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index fca54860..40868e2f 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -31,7 +31,13 @@ from typing import ( TYPE_CHECKING, ) -from poezio import mam, poopt, timed_events, xhtml, windows +from poezio import ( + mam, + poopt, + timed_events, + xhtml, + windows +) from poezio.core.structs import Command, Completion, Status from poezio.common import safeJID from poezio.config import config diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index 2c0d192a..1667f0dc 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -14,90 +14,8 @@ log = logging.getLogger(__name__) from typing import Dict, Union, Optional, List, Tuple from datetime import datetime from poezio.config import config -from poezio.theming import get_theme, dump_tuple - - -class Message: - __slots__ = ('txt', 'nick_color', 'time', 'str_time', 'nickname', 'user', - 'identifier', 'top', 'highlight', 'me', 'old_message', 'revisions', - 'jid', 'ack') - - def __init__(self, - txt: str, - time: Optional[datetime], - nickname: Optional[str], - nick_color: Optional[Tuple], - history: bool, - user: Optional[str], - identifier: Optional[str], - top: Optional[bool] = False, - str_time: Optional[str] = None, - highlight: bool = False, - old_message: Optional['Message'] = None, - revisions: int = 0, - jid: Optional[str] = None, - ack: int = 0) -> None: - """ - Create a new Message object with parameters, check for /me messages, - and delayed messages - """ - time = time if time is not None else datetime.now() - if txt.startswith('/me '): - me = True - txt = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_ME_MESSAGE), - txt[4:]) - else: - me = False - str_time = time.strftime("%H:%M:%S") - if history: - txt = txt.replace( - '\x19o', - '\x19o\x19%s}' % dump_tuple(get_theme().COLOR_LOG_MSG)) - str_time = time.strftime("%Y-%m-%d %H:%M:%S") - - self.txt = txt.replace('\t', ' ') + '\x19o' - self.nick_color = nick_color - self.time = time - self.str_time = str_time - self.nickname = nickname - self.user = user - self.identifier = identifier - self.top = top - self.highlight = highlight - self.me = me - self.old_message = old_message - self.revisions = revisions - self.jid = jid - self.ack = ack - - def _other_elems(self) -> str: - "Helper for the repr_message function" - acc = [] - fields = list(self.__slots__) - fields.remove('old_message') - for field in fields: - acc.append('%s=%s' % (field, repr(getattr(self, field)))) - return 'Message(%s, %s' % (', '.join(acc), 'old_message=') - - def __repr__(self) -> str: - """ - repr() for the Message class, for debug purposes, since the default - repr() is recursive, so it can stack overflow given too many revisions - of a message - """ - init = self._other_elems() - acc = [init] - next_message = self.old_message - rev = 1 - while next_message is not None: - acc.append(next_message._other_elems()) - next_message = next_message.old_message - rev += 1 - acc.append('None') - while rev: - acc.append(')') - rev -= 1 - return ''.join(acc) +from poezio.ui.types import Message + class CorrectionError(Exception): diff --git a/poezio/ui/__init__.py b/poezio/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/poezio/ui/consts.py b/poezio/ui/consts.py new file mode 100644 index 00000000..91f19a82 --- /dev/null +++ b/poezio/ui/consts.py @@ -0,0 +1,4 @@ +FORMAT_CHAR = '\x19' +# These are non-printable chars, so they should never appear in the input, +# I guess. But maybe we can find better chars that are even less risky. +FORMAT_CHARS = '\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x1A' diff --git a/poezio/ui/funcs.py b/poezio/ui/funcs.py new file mode 100644 index 00000000..260cc037 --- /dev/null +++ b/poezio/ui/funcs.py @@ -0,0 +1,60 @@ +""" +Standalone functions used by the modules +""" + +import string +from typing import Optional, List +from poezio.ui.consts import FORMAT_CHAR, FORMAT_CHARS + +DIGITS = string.digits + '-' + + +def find_first_format_char(text: str, + chars: str = None) -> int: + to_find = chars or FORMAT_CHARS + pos = -1 + for char in to_find: + p = text.find(char) + if p == -1: + continue + if pos == -1 or p < pos: + pos = p + return pos + + +def truncate_nick(nick: Optional[str], size=10) -> Optional[str]: + if size < 1: + size = 1 + if nick and len(nick) > size: + return nick[:size] + '…' + return nick + + +def parse_attrs(text: str, previous: Optional[List[str]] = None) -> List[str]: + next_attr_char = text.find(FORMAT_CHAR) + if previous: + attrs = previous + else: + attrs = [] + while next_attr_char != -1 and text: + if next_attr_char + 1 < len(text): + attr_char = text[next_attr_char + 1].lower() + else: + attr_char = '\0' + if attr_char == 'o': + attrs = [] + elif attr_char == 'u': + attrs.append('u') + elif attr_char == 'b': + attrs.append('b') + elif attr_char == 'i': + attrs.append('i') + if attr_char in DIGITS and attr_char: + color_str = text[next_attr_char + 1:text.find('}', next_attr_char)] + if color_str: + attrs.append(color_str + '}') + text = text[next_attr_char + len(color_str) + 2:] + else: + text = text[next_attr_char + 2:] + next_attr_char = text.find(FORMAT_CHAR) + return attrs diff --git a/poezio/ui/types.py b/poezio/ui/types.py new file mode 100644 index 00000000..69d77a07 --- /dev/null +++ b/poezio/ui/types.py @@ -0,0 +1,158 @@ + +from datetime import datetime +from math import ceil, log10 +from typing import Union, Optional, List, Tuple + +from poezio.theming import get_theme, dump_tuple +from poezio.ui.funcs import truncate_nick, parse_attrs +from poezio import poopt +from poezio.ui.consts import FORMAT_CHAR + + +class Message: + __slots__ = ('txt', 'nick_color', 'time', 'str_time', 'nickname', 'user', + 'identifier', 'top', 'highlight', 'me', 'old_message', 'revisions', + 'jid', 'ack') + + def __init__(self, + txt: str, + time: Optional[datetime], + nickname: Optional[str], + nick_color: Optional[Tuple], + history: bool, + user: Optional[str], + identifier: Optional[str], + top: Optional[bool] = False, + str_time: Optional[str] = None, + highlight: bool = False, + old_message: Optional['Message'] = None, + revisions: int = 0, + jid: Optional[str] = None, + ack: int = 0) -> None: + """ + Create a new Message object with parameters, check for /me messages, + and delayed messages + """ + time = time if time is not None else datetime.now() + if txt.startswith('/me '): + me = True + txt = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_ME_MESSAGE), + txt[4:]) + else: + me = False + str_time = time.strftime("%H:%M:%S") + if history: + txt = txt.replace( + '\x19o', + '\x19o\x19%s}' % dump_tuple(get_theme().COLOR_LOG_MSG)) + str_time = time.strftime("%Y-%m-%d %H:%M:%S") + + self.txt = txt.replace('\t', ' ') + '\x19o' + self.nick_color = nick_color + self.time = time + self.str_time = str_time + self.nickname = nickname + self.user = user + self.identifier = identifier + self.top = top + self.highlight = highlight + self.me = me + self.old_message = old_message + self.revisions = revisions + self.jid = jid + self.ack = ack + + def _other_elems(self) -> str: + "Helper for the repr_message function" + acc = [] + fields = list(self.__slots__) + fields.remove('old_message') + for field in fields: + acc.append('%s=%s' % (field, repr(getattr(self, field)))) + return 'Message(%s, %s' % (', '.join(acc), 'old_message=') + + def __repr__(self) -> str: + """ + repr() for the Message class, for debug purposes, since the default + repr() is recursive, so it can stack overflow given too many revisions + of a message + """ + init = self._other_elems() + acc = [init] + next_message = self.old_message + rev = 1 + while next_message is not None: + acc.append(next_message._other_elems()) + next_message = next_message.old_message + rev += 1 + acc.append('None') + while rev: + acc.append(')') + rev -= 1 + return ''.join(acc) + + def render(self, width: int, timestamp: bool = False, nick_size: int = 10) -> List["Line"]: + """ + Build a list of lines from this message. + """ + txt = self.txt + if not txt: + return [] + theme = get_theme() + if len(self.str_time) > 8: + default_color = ( + FORMAT_CHAR + dump_tuple(theme.COLOR_LOG_MSG) + '}') # type: Optional[str] + else: + default_color = None + ret = [] # type: List[Union[None, Line]] + nick = truncate_nick(self.nickname, nick_size) + offset = 0 + if self.ack: + if self.ack > 0: + offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1 + else: + offset += poopt.wcswidth(theme.CHAR_NACK) + 1 + if nick: + offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length + if self.revisions > 0: + offset += ceil(log10(self.revisions + 1)) + if self.me: + offset += 1 # '* ' before and ' ' after + if timestamp: + if self.str_time: + offset += 1 + len(self.str_time) + if theme.CHAR_TIME_LEFT and self.str_time: + offset += 1 + if theme.CHAR_TIME_RIGHT and self.str_time: + offset += 1 + lines = poopt.cut_text(txt, width - offset - 1) + prepend = default_color if default_color else '' + attrs = [] # type: List[str] + for line in lines: + saved = Line( + msg=self, + start_pos=line[0], + end_pos=line[1], + prepend=prepend) + attrs = parse_attrs(self.txt[line[0]:line[1]], attrs) + if attrs: + prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs) + else: + if default_color: + prepend = default_color + else: + prepend = '' + ret.append(saved) + return ret + + +# msg is a reference to the corresponding Message object. text_start and +# text_end are the position delimiting the text in this line. +class Line: + __slots__ = ('msg', 'start_pos', 'end_pos', 'prepend') + + def __init__(self, msg: Message, start_pos: int, end_pos: int, prepend: str) -> None: + self.msg = msg + self.start_pos = start_pos + self.end_pos = end_pos + self.prepend = prepend diff --git a/poezio/windows/base_wins.py b/poezio/windows/base_wins.py index ac6b4804..6a689067 100644 --- a/poezio/windows/base_wins.py +++ b/poezio/windows/base_wins.py @@ -20,10 +20,7 @@ from typing import Optional, Tuple, TYPE_CHECKING from poezio.theming import to_curses_attr, read_tuple -FORMAT_CHAR = '\x19' -# These are non-printable chars, so they should never appear in the input, -# I guess. But maybe we can find better chars that are even less risky. -format_chars = '\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x1A' +from poezio.ui.consts import FORMAT_CHAR if TYPE_CHECKING: from _curses import _CursesWindow # pylint: disable=E0611 diff --git a/poezio/windows/funcs.py b/poezio/windows/funcs.py deleted file mode 100644 index 22977374..00000000 --- a/poezio/windows/funcs.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Standalone functions used by the modules -""" - -import string -from typing import Optional, List -from poezio.windows.base_wins import FORMAT_CHAR, format_chars - -DIGITS = string.digits + '-' - - -def find_first_format_char(text: str, - chars: str = None) -> int: - to_find = chars or format_chars - pos = -1 - for char in to_find: - p = text.find(char) - if p == -1: - continue - if pos == -1 or p < pos: - pos = p - return pos - - -def truncate_nick(nick: Optional[str], size=10) -> Optional[str]: - if size < 1: - size = 1 - if nick and len(nick) > size: - return nick[:size] + '…' - return nick - - -def parse_attrs(text: str, previous: Optional[List[str]] = None) -> List[str]: - next_attr_char = text.find(FORMAT_CHAR) - if previous: - attrs = previous - else: - attrs = [] - while next_attr_char != -1 and text: - if next_attr_char + 1 < len(text): - attr_char = text[next_attr_char + 1].lower() - else: - attr_char = '\0' - if attr_char == 'o': - attrs = [] - elif attr_char == 'u': - attrs.append('u') - elif attr_char == 'b': - attrs.append('b') - elif attr_char == 'i': - attrs.append('i') - if attr_char in DIGITS and attr_char: - color_str = text[next_attr_char + 1:text.find('}', next_attr_char)] - if color_str: - attrs.append(color_str + '}') - text = text[next_attr_char + len(color_str) + 2:] - else: - text = text[next_attr_char + 2:] - next_attr_char = text.find(FORMAT_CHAR) - return attrs diff --git a/poezio/windows/info_wins.py b/poezio/windows/info_wins.py index 3a8d1863..d31130fe 100644 --- a/poezio/windows/info_wins.py +++ b/poezio/windows/info_wins.py @@ -10,7 +10,7 @@ from poezio.common import safeJID from poezio.config import config from poezio.windows.base_wins import Win -from poezio.windows.funcs import truncate_nick +from poezio.ui.funcs import truncate_nick from poezio.theming import get_theme, to_curses_attr diff --git a/poezio/windows/inputs.py b/poezio/windows/inputs.py index 84b95599..5cca8803 100644 --- a/poezio/windows/inputs.py +++ b/poezio/windows/inputs.py @@ -10,8 +10,9 @@ from typing import List, Dict, Callable, Optional from poezio import keyboard from poezio import common from poezio import poopt -from poezio.windows.base_wins import Win, format_chars -from poezio.windows.funcs import find_first_format_char +from poezio.windows.base_wins import Win +from poezio.ui.consts import FORMAT_CHARS +from poezio.ui.funcs import find_first_format_char from poezio.config import config from poezio.theming import to_curses_attr @@ -487,7 +488,7 @@ class Input(Win): (\x0E to \x19 instead of \x19 + attr). We do not use any } char in this version """ - chars = format_chars + '\n' + chars = FORMAT_CHARS + '\n' if y is not None and x is not None: self.move(y, x) format_char = find_first_format_char(text, chars) @@ -497,7 +498,7 @@ class Input(Win): if text[format_char] == '\n': attr_char = '|' else: - attr_char = self.text_attributes[format_chars.index( + attr_char = self.text_attributes[FORMAT_CHARS.index( text[format_char])] self.addstr(text[:format_char]) self.addstr(attr_char, curses.A_REVERSE) @@ -696,7 +697,7 @@ class MessageInput(HistoryInput): def cb(attr_char): if attr_char in self.text_attributes: - char = format_chars[self.text_attributes.index(attr_char)] + char = FORMAT_CHARS[self.text_attributes.index(attr_char)] self.do_command(char, False) self.rewrite_text() diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py index f4c78c2c..bba66d60 100644 --- a/poezio/windows/text_win.py +++ b/poezio/windows/text_win.py @@ -9,28 +9,16 @@ from math import ceil, log10 from typing import Optional, List, Union from poezio.windows.base_wins import Win, FORMAT_CHAR -from poezio.windows.funcs import truncate_nick, parse_attrs +from poezio.ui.funcs import truncate_nick, parse_attrs from poezio import poopt from poezio.config import config from poezio.theming import to_curses_attr, get_theme, dump_tuple -from poezio.text_buffer import Message +from poezio.ui.types import Line, Message log = logging.getLogger(__name__) -# msg is a reference to the corresponding Message object. text_start and -# text_end are the position delimiting the text in this line. -class Line: - __slots__ = ('msg', 'start_pos', 'end_pos', 'prepend') - - def __init__(self, msg: Message, start_pos: int, end_pos: int, prepend: str) -> None: - self.msg = msg - self.start_pos = start_pos - self.end_pos = end_pos - self.prepend = prepend - - class BaseTextWin(Win): __slots__ = ('lines_nb_limit', 'pos', 'built_lines', 'lock', 'lock_buffer', 'separator_after') @@ -360,55 +348,7 @@ class TextWin(BaseTextWin): """ if message is None: # line separator return [None] - txt = message.txt - if not txt: - return [] - theme = get_theme() - if len(message.str_time) > 8: - default_color = ( - FORMAT_CHAR + dump_tuple(theme.COLOR_LOG_MSG) + '}') # type: Optional[str] - else: - default_color = None - ret = [] # type: List[Union[None, Line]] - nick = truncate_nick(message.nickname, nick_size) - offset = 0 - if message.ack: - if message.ack > 0: - offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1 - else: - offset += poopt.wcswidth(theme.CHAR_NACK) + 1 - if nick: - offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length - if message.revisions > 0: - offset += ceil(log10(message.revisions + 1)) - if message.me: - offset += 1 # '* ' before and ' ' after - if timestamp: - if message.str_time: - offset += 1 + len(message.str_time) - if theme.CHAR_TIME_LEFT and message.str_time: - offset += 1 - if theme.CHAR_TIME_RIGHT and message.str_time: - offset += 1 - lines = poopt.cut_text(txt, self.width - offset - 1) - prepend = default_color if default_color else '' - attrs = [] # type: List[str] - for line in lines: - saved = Line( - msg=message, - start_pos=line[0], - end_pos=line[1], - prepend=prepend) - attrs = parse_attrs(message.txt[line[0]:line[1]], attrs) - if attrs: - prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs) - else: - if default_color: - prepend = default_color - else: - prepend = '' - ret.append(saved) - return ret + return message.render(self.width, timestamp, nick_size) def refresh(self) -> None: log.debug('Refresh: %s', self.__class__.__name__) diff --git a/poezio/xhtml.py b/poezio/xhtml.py index 899985ef..0b234c29 100644 --- a/poezio/xhtml.py +++ b/poezio/xhtml.py @@ -488,7 +488,7 @@ def convert_simple_to_full_colors(text: str) -> str: a \x19n} formatted one. """ # TODO, have a single list of this. This is some sort of - # duplicate from windows.format_chars + # duplicate from ui.consts.FORMAT_CHARS mapping = str.maketrans({ '\x0E': '\x19b', '\x0F': '\x19o', -- cgit v1.2.3