summaryrefslogtreecommitdiff
path: root/poezio/ui/render.py
diff options
context:
space:
mode:
Diffstat (limited to 'poezio/ui/render.py')
-rw-r--r--poezio/ui/render.py280
1 files changed, 280 insertions, 0 deletions
diff --git a/poezio/ui/render.py b/poezio/ui/render.py
new file mode 100644
index 00000000..aad482b5
--- /dev/null
+++ b/poezio/ui/render.py
@@ -0,0 +1,280 @@
+from __future__ import annotations
+
+import curses
+
+from datetime import (
+ datetime,
+ date,
+)
+from functools import singledispatch
+from math import ceil, log10
+from typing import (
+ List,
+ Optional,
+ Tuple,
+ TYPE_CHECKING,
+)
+
+from poezio import poopt
+from poezio.theming import (
+ get_theme,
+)
+from poezio.ui.consts import (
+ FORMAT_CHAR,
+)
+from poezio.ui.funcs import (
+ truncate_nick,
+ parse_attrs,
+)
+from poezio.ui.types import (
+ BaseMessage,
+ Message,
+ StatusMessage,
+ UIMessage,
+ XMLLog,
+)
+
+if TYPE_CHECKING:
+ from poezio.windows import Win
+
+# 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: 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)
+ generated_lines = generate_lines(lines, msg, default_color='')
+ return generated_lines
+
+
+@build_lines.register(StatusMessage)
+def build_status(msg: StatusMessage, width: int, timestamp: bool, nick_size: int = 10) -> List[Line]:
+ msg.rebuild()
+ 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(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: Win, with_timestamps: bool, nick_size: int) -> int:
+ """Write the part before text (only the timestamp)"""
+ if with_timestamps:
+ return PreMessageHelpers.write_time(win, False, msg.time)
+ return 0
+
+
+@write_pre.register(UIMessage)
+def write_pre_uimessage(msg: UIMessage, win: Win, with_timestamps: bool, nick_size: int) -> int:
+ """ Write the prefix of a ui message log
+ - timestamp (short or long)
+ - level
+ """
+ color: Optional[Tuple]
+ offset = 0
+ if with_timestamps:
+ offset += PreMessageHelpers.write_time(win, False, msg.time)
+
+ if not msg.level: # not a message, nothing to do afterwards
+ return offset
+
+ level = truncate_nick(msg.level, nick_size)
+ offset += poopt.wcswidth(level)
+ color = msg.color
+ PreMessageHelpers.write_nickname(win, level, color, False)
+ win.addstr('> ')
+ offset += 2
+ return offset
+
+
+@write_pre.register(Message)
+def write_pre_message(msg: Message, win: 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
+ """
+ color: Optional[Tuple]
+ offset = 0
+ if with_timestamps:
+ 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)
+ theme = get_theme()
+ if msg.me:
+ with win.colored_text(color=theme.COLOR_ME_MESSAGE):
+ win.addstr(theme.CHAR_BEFORE_NICK_ME)
+ PreMessageHelpers.write_nickname(win, nick, color, msg.highlight)
+ offset += PreMessageHelpers.write_revisions(win, msg)
+ win.addstr(theme.CHAR_AFTER_NICK_ME)
+ offset += len(theme.CHAR_BEFORE_NICK_ME) + len(theme.CHAR_AFTER_NICK_ME)
+ else:
+ PreMessageHelpers.write_nickname(win, nick, color, msg.highlight)
+ offset += PreMessageHelpers.write_revisions(win, msg)
+ win.addstr(theme.CHAR_AFTER_NICK)
+ offset += len(theme.CHAR_AFTER_NICK)
+ return offset
+
+
+@write_pre.register(XMLLog)
+def write_pre_xmllog(msg: XMLLog, win: Win, with_timestamps: bool, nick_size: int) -> int:
+ """Write the part before the stanza (timestamp + IN/OUT)"""
+ offset = 0
+ if with_timestamps:
+ offset += 1 + 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: Win, 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: Win) -> 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: Win) -> 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: Win, 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: Win, history: bool, time: datetime) -> int:
+ """
+ Write the date on the yth line of the window
+ """
+ if time:
+ theme = get_theme()
+ if history and time.date() != date.today():
+ format = theme.LONG_TIME_FORMAT
+ else:
+ format = theme.SHORT_TIME_FORMAT
+ time_str = time.strftime(format)
+ color = theme.COLOR_TIME_STRING
+ with buffer.colored_text(color=color):
+ buffer.addstr(time_str)
+ buffer.addstr(' ')
+ return poopt.wcswidth(time_str) + 1
+ return 0