summaryrefslogtreecommitdiff
path: root/poezio/ui
diff options
context:
space:
mode:
authormathieui <mathieui@mathieui.net>2019-09-28 18:35:23 +0200
committermathieui <mathieui@mathieui.net>2020-05-09 19:46:17 +0200
commit80ce8453f50ccaad4d71fda8811ee33f5ffa3624 (patch)
tree6e4b7f86f97a82c5fd4cebc6a61349283dee2a36 /poezio/ui
parenta5ef6ec9105f22d14b7d7ec3b634796fc3466e93 (diff)
downloadpoezio-80ce8453f50ccaad4d71fda8811ee33f5ffa3624.tar.gz
poezio-80ce8453f50ccaad4d71fda8811ee33f5ffa3624.tar.bz2
poezio-80ce8453f50ccaad4d71fda8811ee33f5ffa3624.tar.xz
poezio-80ce8453f50ccaad4d71fda8811ee33f5ffa3624.zip
Rewrite part of the message handling/rendering
Diffstat (limited to 'poezio/ui')
-rw-r--r--poezio/ui/consts.py10
-rw-r--r--poezio/ui/funcs.py10
-rw-r--r--poezio/ui/render.py234
-rw-r--r--poezio/ui/types.py172
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