Commit 80ce8453 authored by mathieui's avatar mathieui

Rewrite part of the message handling/rendering

parent a5ef6ec9
......@@ -1349,7 +1349,10 @@ class Core:
colors = get_theme().INFO_COLORS
color = colors.get(typ.lower(), colors.get('default', None))
nb_lines = self.information_buffer.add_message(
msg, nickname=typ, nick_color=color)
txt=msg,
nickname=typ,
nick_color=color
)
popup_on = config.get('information_buffer_popup_on').split()
if isinstance(self.tabs.current_tab, tabs.RosterInfoTab):
self.refresh_window()
......
......@@ -72,7 +72,6 @@ def add_line(
highlight=False,
top=top,
identifier=None,
str_time=None,
jid=None,
)
......
......@@ -45,7 +45,8 @@ from poezio.decorators import command_args_parser, refresh_wrapper
from poezio.logger import logger
from poezio.text_buffer import TextBuffer
from poezio.theming import get_theme, dump_tuple
from poezio.windows.funcs import truncate_nick
from poezio.ui.funcs import truncate_nick
from poezio.ui.consts import LONG_FORMAT_LENGTH
from slixmpp import JID, InvalidJID, Message
......@@ -839,12 +840,8 @@ class ChatTab(Tab):
if message.me:
offset += 1
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
if message.history:
offset += 1 + LONG_FORMAT_LENGTH
lines = poopt.cut_text(txt, self.text_win.width - offset - 1)
for line in lines:
built_lines.append(line)
......
......@@ -262,7 +262,10 @@ class XMLTab(Tab):
else:
xml = self.core_buffer.messages[:]
text = '\n'.join(
('%s %s %s' % (msg.str_time, msg.nickname, clean_text(msg.txt))
('%s %s %s' % (
msg.time.strftime('%H:%M:%S'),
msg.nickname,
clean_text(msg.txt))
for msg in xml))
filename = os.path.expandvars(os.path.expanduser(args[0]))
try:
......
......@@ -63,7 +63,6 @@ class TextBuffer:
highlight: bool = False,
top: Optional[bool] = False,
identifier: Optional[str] = None,
str_time: Optional[str] = None,
jid: Optional[str] = None,
ack: int = 0) -> int:
"""
......@@ -71,14 +70,13 @@ class TextBuffer:
"""
msg = Message(
txt,
time,
nickname,
nick_color,
history,
user,
identifier,
top,
str_time=str_time,
time=time,
nickname=nickname,
nick_color=nick_color,
history=history,
user=user,
identifier=identifier,
top=top,
highlight=highlight,
jid=jid,
ack=ack)
......@@ -180,7 +178,7 @@ class TextBuffer:
if msg.user and msg.user is not user:
raise CorrectionError("Different users")
elif len(msg.str_time) > 8: # ugly
elif msg.history:
raise CorrectionError("Delayed message")
elif not msg.user and (msg.jid is None or jid is None):
raise CorrectionError('Could not check the '
......@@ -195,13 +193,13 @@ class TextBuffer:
self.correction_ids[new_id] = orig_id
message = Message(
txt,
time,
msg.nickname,
msg.nick_color,
False,
msg.user,
orig_id,
txt=txt,
time=time,
nickname=msg.nickname,
nick_color=msg.nick_color,
history=False,
user=msg.user,
identifier=orig_id,
highlight=highlight,
old_message=msg,
revisions=msg.revisions + 1,
......
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))
......@@ -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]:
......
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
......@@ -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
......@@ -14,7 +14,8 @@ 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.ui.types import Line, Message
from poezio.ui.types import Message
from poezio.ui.render import Line, build_lines, write_pre
log = logging.getLogger(__name__)
......@@ -106,7 +107,7 @@ class BaseTextWin(Win):
Build a list of lines from a message, without adding it
to a list
"""
return []
return build_lines(message, self.width, timestamp, nick_size)
def refresh(self) -> None:
pass
......@@ -117,20 +118,6 @@ class BaseTextWin(Win):
"""
self.addstr_colored(txt, y, x)
def write_time(self, time: str) -> int:
"""
Write the date on the yth line of the window
"""
if time:
color = get_theme().COLOR_TIME_STRING
curses_color = to_curses_attr(color)
self._win.attron(curses_color)
self.addstr(time)