diff options
Diffstat (limited to 'poezio/ui')
-rw-r--r-- | poezio/ui/consts.py | 10 | ||||
-rw-r--r-- | poezio/ui/funcs.py | 10 | ||||
-rw-r--r-- | poezio/ui/render.py | 234 | ||||
-rw-r--r-- | poezio/ui/types.py | 172 |
4 files changed, 339 insertions, 87 deletions
diff --git a/poezio/ui/consts.py b/poezio/ui/consts.py index 91f19a82..0838d953 100644 --- a/poezio/ui/consts.py +++ b/poezio/ui/consts.py @@ -1,4 +1,14 @@ +from datetime import datetime + 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' + +# Short date format (only show time) +SHORT_FORMAT = '%H:%M:%S' +SHORT_FORMAT_LENGTH = len(datetime.now().strftime(SHORT_FORMAT)) + +# Long date format (show date and time) +LONG_FORMAT = '%Y-%m-%d %H:%M:%S' +LONG_FORMAT_LENGTH = len(datetime.now().strftime(LONG_FORMAT)) diff --git a/poezio/ui/funcs.py b/poezio/ui/funcs.py index 260cc037..023432ee 100644 --- a/poezio/ui/funcs.py +++ b/poezio/ui/funcs.py @@ -22,12 +22,14 @@ def find_first_format_char(text: str, return pos -def truncate_nick(nick: Optional[str], size=10) -> Optional[str]: +def truncate_nick(nick: Optional[str], size=10) -> str: if size < 1: size = 1 - if nick and len(nick) > size: - return nick[:size] + '…' - return nick + if nick: + if len(nick) > size: + return nick[:size] + '…' + return nick + return '' def parse_attrs(text: str, previous: Optional[List[str]] = None) -> List[str]: diff --git a/poezio/ui/render.py b/poezio/ui/render.py new file mode 100644 index 00000000..64b480a9 --- /dev/null +++ b/poezio/ui/render.py @@ -0,0 +1,234 @@ +import logging +import curses + +from datetime import datetime +from functools import singledispatch +from typing import List, Optional, Tuple +from math import ceil, log10 + +from poezio import poopt +from poezio.ui.consts import ( + FORMAT_CHAR, + LONG_FORMAT, + SHORT_FORMAT, +) +from poezio.ui.funcs import ( + truncate_nick, + parse_attrs, +) +from poezio.theming import ( + get_theme, +) +from poezio.ui.types import ( + BaseMessage, + Message, + XMLLog, +) + +# 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: BaseMessage, 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 + + def __repr__(self): + return '(%s, %s)' % (self.start_pos, self.end_pos) + + +LinePos = Tuple[int, int] + +def generate_lines(lines: List[LinePos], msg: BaseMessage, default_color: str = '') -> List[Line]: + line_objects = [] + attrs = [] # type: List[str] + prepend = default_color if default_color else '' + for line in lines: + saved = Line( + msg=msg, + start_pos=line[0], + end_pos=line[1], + prepend=prepend) + attrs = parse_attrs(msg.txt[line[0]:line[1]], attrs) + if attrs: + prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs) + else: + if default_color: + prepend = default_color + else: + prepend = '' + line_objects.append(saved) + return line_objects + + +@singledispatch +def build_lines(msg: BaseMessage, width: int, timestamp: bool, nick_size: int = 10) -> List[Line]: + offset = msg.compute_offset(timestamp, nick_size) + lines = poopt.cut_text(msg.txt, width - offset - 1) + return generate_lines(lines, msg, default_color='') + + +@build_lines.register(type(None)) +def build_separator(*args, **kwargs): + return [None] + + +@build_lines.register(Message) +def build_message(msg: Message, width: int, timestamp: bool, nick_size: int = 10) -> List[Line]: + """ + Build a list of lines from this message. + """ + txt = msg.txt + if not txt: + return [] + offset = msg.compute_offset(timestamp, nick_size) + lines = poopt.cut_text(txt, width - offset - 1) + return generate_lines(lines, msg, default_color='') + + +@build_lines.register(XMLLog) +def build_xmllog(msg: XMLLog, width: int, timestamp: bool, nick_size: int = 10) -> List[Line]: + offset = msg.compute_offset(timestamp, nick_size) + lines = poopt.cut_text(msg.txt, width - offset - 1) + return generate_lines(lines, msg, default_color='') + + +@singledispatch +def write_pre(msg: BaseMessage, win, with_timestamps: bool, nick_size: int) -> int: + """Write the part before text (only the timestamp)""" + if with_timestamps: + return 1 + PreMessageHelpers.write_time(win, False, msg.time) + return 0 + + +@write_pre.register(Message) +def write_pre_message(msg: Message, win, with_timestamps: bool, nick_size: int) -> int: + """Write the part before the body: + - timestamp (short or long) + - ack/nack + - nick (with a "* " for /me) + - LMC number if present + """ + offset = 0 + if with_timestamps: + logging.debug(msg) + offset += PreMessageHelpers.write_time(win, msg.history, msg.time) + + if not msg.nickname: # not a message, nothing to do afterwards + return offset + + nick = truncate_nick(msg.nickname, nick_size) + offset += poopt.wcswidth(nick) + if msg.nick_color: + color = msg.nick_color + elif msg.user: + color = msg.user.color + else: + color = None + if msg.ack: + if msg.ack > 0: + offset += PreMessageHelpers.write_ack(win) + else: + offset += PreMessageHelpers.write_nack(win) + if msg.me: + with win.colored_text(color=get_theme().COLOR_ME_MESSAGE): + win.addstr('* ') + PreMessageHelpers.write_nickname(win, nick, color, msg.highlight) + offset += PreMessageHelpers.write_revisions(win, msg) + win.addstr(' ') + offset += 3 + else: + PreMessageHelpers.write_nickname(win, nick, color, msg.highlight) + offset += PreMessageHelpers.write_revisions(win, msg) + win.addstr('> ') + offset += 2 + return offset + + +@write_pre.register(XMLLog) +def write_pre_xmllog(msg: XMLLog, win, with_timestamps: bool, nick_size: int) -> int: + """Write the part before the stanza (timestamp + IN/OUT)""" + offset = 0 + if with_timestamps: + offset += PreMessageHelpers.write_time(win, False, msg.time) + theme = get_theme() + if msg.incoming: + char = theme.CHAR_XML_IN + color = theme.COLOR_XML_IN + else: + char = theme.CHAR_XML_OUT + color = theme.COLOR_XML_OUT + nick = truncate_nick(char, nick_size) + offset += poopt.wcswidth(nick) + PreMessageHelpers.write_nickname(win, char, color) + win.addstr(' ') + return offset + +class PreMessageHelpers: + + @staticmethod + def write_revisions(buffer, msg: Message) -> int: + if msg.revisions: + color = get_theme().COLOR_REVISIONS_MESSAGE + with buffer.colored_text(color=color): + buffer.addstr('%d' % msg.revisions) + return ceil(log10(msg.revisions + 1)) + return 0 + + @staticmethod + def write_ack(buffer) -> int: + theme = get_theme() + color = theme.COLOR_CHAR_ACK + with buffer.colored_text(color=color): + buffer.addstr(theme.CHAR_ACK_RECEIVED) + buffer.addstr(' ') + return poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1 + + @staticmethod + def write_nack(buffer) -> int: + theme = get_theme() + color = theme.COLOR_CHAR_NACK + with buffer.colored_text(color=color): + buffer.addstr(theme.CHAR_NACK) + buffer.addstr(' ') + return poopt.wcswidth(theme.CHAR_NACK) + 1 + + @staticmethod + def write_nickname(buffer, nickname: str, color, highlight=False) -> None: + """ + Write the nickname, using the user's color + and return the number of written characters + """ + if not nickname: + return + attr = None + if highlight: + hl_color = get_theme().COLOR_HIGHLIGHT_NICK + if hl_color == "reverse": + attr = curses.A_REVERSE + else: + color = hl_color + with buffer.colored_text(color=color, attr=attr): + buffer.addstr(nickname) + + @staticmethod + def write_time(buffer, history: bool, time: datetime) -> int: + """ + Write the date on the yth line of the window + """ + if time: + if history: + format = LONG_FORMAT + else: + format = SHORT_FORMAT + logging.debug(time) + time_str = time.strftime(format) + color = get_theme().COLOR_TIME_STRING + with buffer.colored_text(color=color): + buffer.addstr(time_str) + buffer.addstr(' ') + return poopt.wcswidth(time_str) + 1 + return 0 diff --git a/poezio/ui/types.py b/poezio/ui/types.py index 69d77a07..18e51427 100644 --- a/poezio/ui/types.py +++ b/poezio/ui/types.py @@ -2,28 +2,78 @@ 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.ui.funcs import truncate_nick from poezio import poopt -from poezio.ui.consts import FORMAT_CHAR +from poezio.user import User +from poezio.theming import dump_tuple, get_theme +from poezio.ui.consts import ( + SHORT_FORMAT_LENGTH, + LONG_FORMAT_LENGTH, +) + + +class BaseMessage: + __slots__ = ('txt', 'time', 'identifier') + + def __init__(self, txt: str, identifier: str = '', time: Optional[datetime] = None): + self.txt = txt + self.identifier = identifier + if time is not None: + self.time = time + else: + self.time = datetime.now() + + def compute_offset(self, with_timestamps: bool, nick_size: int) -> int: + return SHORT_FORMAT_LENGTH + 1 + + +class XMLLog(BaseMessage): + """XML Log message""" + __slots__ = ('txt', 'time', 'identifier', 'incoming') + + def __init__( + self, + txt: str, + incoming: bool, + ): + BaseMessage.__init__( + self, + txt=txt, + identifier='', + ) + self.txt = txt + self.identifier = '' + self.incoming = incoming + + def compute_offset(self, with_timestamps: bool, nick_size: int) -> int: + offset = 0 + theme = get_theme() + IN, OUT = theme.CHAR_XML_IN, theme.CHAR_XML_OUT + if with_timestamps: + offset += 1 + SHORT_FORMAT_LENGTH + if self.incoming: + nick = IN + else: + nick = OUT + nick = truncate_nick(nick, nick_size) or '' + offset += 1 + len(nick) + return offset -class Message: - __slots__ = ('txt', 'nick_color', 'time', 'str_time', 'nickname', 'user', +class Message(BaseMessage): + __slots__ = ('txt', 'nick_color', 'time', 'nickname', 'user', 'history', '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], + time: Optional[datetime] = None, + nick_color: Optional[Tuple] = None, + history: bool = False, + user: Optional[User] = None, + identifier: Optional[str] = '', top: Optional[bool] = False, - str_time: Optional[str] = None, highlight: bool = False, old_message: Optional['Message'] = None, revisions: int = 0, @@ -33,27 +83,22 @@ class Message: Create a new Message object with parameters, check for /me messages, and delayed messages """ - time = time if time is not None else datetime.now() + BaseMessage.__init__( + self, + txt=txt.replace('\t', ' ') + '\x19o', + identifier=identifier or '', + time=time, + ) 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.history = history self.nickname = nickname + self.nick_color = nick_color self.user = user - self.identifier = identifier self.top = top self.highlight = highlight self.me = me @@ -91,68 +136,29 @@ class Message: 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) + def compute_offset(self, with_timestamps: bool, nick_size: int) -> int: offset = 0 + if with_timestamps: + if self.history: + offset += 1 + LONG_FORMAT_LENGTH + else: + offset += 1 + SHORT_FORMAT_LENGTH + + if not self.nickname: # not a message, nothing to do afterwards + return offset + + nick = truncate_nick(self.nickname, nick_size) or '' + offset += poopt.wcswidth(nick) if self.ack: + theme = get_theme() 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 + offset += 3 + else: + offset += 2 + if self.revisions: + offset += ceil(log10(self.revisions + 1)) + return offset |