summaryrefslogtreecommitdiff
path: root/poezio/ui
diff options
context:
space:
mode:
authormathieui <mathieui@mathieui.net>2019-09-22 17:35:07 +0200
committermathieui <mathieui@mathieui.net>2020-05-09 19:46:17 +0200
commit41127e50abc3e126f953af5ad638f92d0848f9f1 (patch)
treeebc7c7a4557e1d005f2bf5a52b84f460342dca63 /poezio/ui
parentd22b4b8c218cbbaee62002d751bd69bfe1d1deab (diff)
downloadpoezio-41127e50abc3e126f953af5ad638f92d0848f9f1.tar.gz
poezio-41127e50abc3e126f953af5ad638f92d0848f9f1.tar.bz2
poezio-41127e50abc3e126f953af5ad638f92d0848f9f1.tar.xz
poezio-41127e50abc3e126f953af5ad638f92d0848f9f1.zip
Move message rendering code to Message.render()
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
Diffstat (limited to 'poezio/ui')
-rw-r--r--poezio/ui/__init__.py0
-rw-r--r--poezio/ui/consts.py4
-rw-r--r--poezio/ui/funcs.py60
-rw-r--r--poezio/ui/types.py158
4 files changed, 222 insertions, 0 deletions
diff --git a/poezio/ui/__init__.py b/poezio/ui/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/poezio/ui/__init__.py
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