Rewrite part of the message handling/rendering
This commit is contained in:
parent
a5ef6ec910
commit
80ce8453f5
10 changed files with 374 additions and 310 deletions
|
@ -1349,7 +1349,10 @@ class Core:
|
||||||
colors = get_theme().INFO_COLORS
|
colors = get_theme().INFO_COLORS
|
||||||
color = colors.get(typ.lower(), colors.get('default', None))
|
color = colors.get(typ.lower(), colors.get('default', None))
|
||||||
nb_lines = self.information_buffer.add_message(
|
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()
|
popup_on = config.get('information_buffer_popup_on').split()
|
||||||
if isinstance(self.tabs.current_tab, tabs.RosterInfoTab):
|
if isinstance(self.tabs.current_tab, tabs.RosterInfoTab):
|
||||||
self.refresh_window()
|
self.refresh_window()
|
||||||
|
|
|
@ -72,7 +72,6 @@ def add_line(
|
||||||
highlight=False,
|
highlight=False,
|
||||||
top=top,
|
top=top,
|
||||||
identifier=None,
|
identifier=None,
|
||||||
str_time=None,
|
|
||||||
jid=None,
|
jid=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,8 @@ from poezio.decorators import command_args_parser, refresh_wrapper
|
||||||
from poezio.logger import logger
|
from poezio.logger import logger
|
||||||
from poezio.text_buffer import TextBuffer
|
from poezio.text_buffer import TextBuffer
|
||||||
from poezio.theming import get_theme, dump_tuple
|
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
|
from slixmpp import JID, InvalidJID, Message
|
||||||
|
|
||||||
|
@ -839,12 +840,8 @@ class ChatTab(Tab):
|
||||||
if message.me:
|
if message.me:
|
||||||
offset += 1
|
offset += 1
|
||||||
if timestamp:
|
if timestamp:
|
||||||
if message.str_time:
|
if message.history:
|
||||||
offset += 1 + len(message.str_time)
|
offset += 1 + LONG_FORMAT_LENGTH
|
||||||
if theme.CHAR_TIME_LEFT and message.str_time:
|
|
||||||
offset += 1
|
|
||||||
if theme.CHAR_TIME_RIGHT and message.str_time:
|
|
||||||
offset += 1
|
|
||||||
lines = poopt.cut_text(txt, self.text_win.width - offset - 1)
|
lines = poopt.cut_text(txt, self.text_win.width - offset - 1)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
built_lines.append(line)
|
built_lines.append(line)
|
||||||
|
|
|
@ -262,7 +262,10 @@ class XMLTab(Tab):
|
||||||
else:
|
else:
|
||||||
xml = self.core_buffer.messages[:]
|
xml = self.core_buffer.messages[:]
|
||||||
text = '\n'.join(
|
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))
|
for msg in xml))
|
||||||
filename = os.path.expandvars(os.path.expanduser(args[0]))
|
filename = os.path.expandvars(os.path.expanduser(args[0]))
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -63,7 +63,6 @@ class TextBuffer:
|
||||||
highlight: bool = False,
|
highlight: bool = False,
|
||||||
top: Optional[bool] = False,
|
top: Optional[bool] = False,
|
||||||
identifier: Optional[str] = None,
|
identifier: Optional[str] = None,
|
||||||
str_time: Optional[str] = None,
|
|
||||||
jid: Optional[str] = None,
|
jid: Optional[str] = None,
|
||||||
ack: int = 0) -> int:
|
ack: int = 0) -> int:
|
||||||
"""
|
"""
|
||||||
|
@ -71,14 +70,13 @@ class TextBuffer:
|
||||||
"""
|
"""
|
||||||
msg = Message(
|
msg = Message(
|
||||||
txt,
|
txt,
|
||||||
time,
|
time=time,
|
||||||
nickname,
|
nickname=nickname,
|
||||||
nick_color,
|
nick_color=nick_color,
|
||||||
history,
|
history=history,
|
||||||
user,
|
user=user,
|
||||||
identifier,
|
identifier=identifier,
|
||||||
top,
|
top=top,
|
||||||
str_time=str_time,
|
|
||||||
highlight=highlight,
|
highlight=highlight,
|
||||||
jid=jid,
|
jid=jid,
|
||||||
ack=ack)
|
ack=ack)
|
||||||
|
@ -180,7 +178,7 @@ class TextBuffer:
|
||||||
|
|
||||||
if msg.user and msg.user is not user:
|
if msg.user and msg.user is not user:
|
||||||
raise CorrectionError("Different users")
|
raise CorrectionError("Different users")
|
||||||
elif len(msg.str_time) > 8: # ugly
|
elif msg.history:
|
||||||
raise CorrectionError("Delayed message")
|
raise CorrectionError("Delayed message")
|
||||||
elif not msg.user and (msg.jid is None or jid is None):
|
elif not msg.user and (msg.jid is None or jid is None):
|
||||||
raise CorrectionError('Could not check the '
|
raise CorrectionError('Could not check the '
|
||||||
|
@ -195,13 +193,13 @@ class TextBuffer:
|
||||||
|
|
||||||
self.correction_ids[new_id] = orig_id
|
self.correction_ids[new_id] = orig_id
|
||||||
message = Message(
|
message = Message(
|
||||||
txt,
|
txt=txt,
|
||||||
time,
|
time=time,
|
||||||
msg.nickname,
|
nickname=msg.nickname,
|
||||||
msg.nick_color,
|
nick_color=msg.nick_color,
|
||||||
False,
|
history=False,
|
||||||
msg.user,
|
user=msg.user,
|
||||||
orig_id,
|
identifier=orig_id,
|
||||||
highlight=highlight,
|
highlight=highlight,
|
||||||
old_message=msg,
|
old_message=msg,
|
||||||
revisions=msg.revisions + 1,
|
revisions=msg.revisions + 1,
|
||||||
|
|
|
@ -1,4 +1,14 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
FORMAT_CHAR = '\x19'
|
FORMAT_CHAR = '\x19'
|
||||||
# These are non-printable chars, so they should never appear in the input,
|
# 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.
|
# 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'
|
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
|
return pos
|
||||||
|
|
||||||
|
|
||||||
def truncate_nick(nick: Optional[str], size=10) -> Optional[str]:
|
def truncate_nick(nick: Optional[str], size=10) -> str:
|
||||||
if size < 1:
|
if size < 1:
|
||||||
size = 1
|
size = 1
|
||||||
if nick and len(nick) > size:
|
if nick:
|
||||||
return nick[:size] + '…'
|
if len(nick) > size:
|
||||||
return nick
|
return nick[:size] + '…'
|
||||||
|
return nick
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def parse_attrs(text: str, previous: Optional[List[str]] = None) -> List[str]:
|
def parse_attrs(text: str, previous: Optional[List[str]] = None) -> List[str]:
|
||||||
|
|
234
poezio/ui/render.py
Normal file
234
poezio/ui/render.py
Normal file
|
@ -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
|
|
@ -2,28 +2,78 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from math import ceil, log10
|
from math import ceil, log10
|
||||||
from typing import Union, Optional, List, Tuple
|
from typing import Union, Optional, List, Tuple
|
||||||
|
from poezio.ui.funcs import truncate_nick
|
||||||
from poezio.theming import get_theme, dump_tuple
|
|
||||||
from poezio.ui.funcs import truncate_nick, parse_attrs
|
|
||||||
from poezio import poopt
|
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 Message:
|
class BaseMessage:
|
||||||
__slots__ = ('txt', 'nick_color', 'time', 'str_time', 'nickname', 'user',
|
__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(BaseMessage):
|
||||||
|
__slots__ = ('txt', 'nick_color', 'time', 'nickname', 'user', 'history',
|
||||||
'identifier', 'top', 'highlight', 'me', 'old_message', 'revisions',
|
'identifier', 'top', 'highlight', 'me', 'old_message', 'revisions',
|
||||||
'jid', 'ack')
|
'jid', 'ack')
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
txt: str,
|
txt: str,
|
||||||
time: Optional[datetime],
|
|
||||||
nickname: Optional[str],
|
nickname: Optional[str],
|
||||||
nick_color: Optional[Tuple],
|
time: Optional[datetime] = None,
|
||||||
history: bool,
|
nick_color: Optional[Tuple] = None,
|
||||||
user: Optional[str],
|
history: bool = False,
|
||||||
identifier: Optional[str],
|
user: Optional[User] = None,
|
||||||
|
identifier: Optional[str] = '',
|
||||||
top: Optional[bool] = False,
|
top: Optional[bool] = False,
|
||||||
str_time: Optional[str] = None,
|
|
||||||
highlight: bool = False,
|
highlight: bool = False,
|
||||||
old_message: Optional['Message'] = None,
|
old_message: Optional['Message'] = None,
|
||||||
revisions: int = 0,
|
revisions: int = 0,
|
||||||
|
@ -33,27 +83,22 @@ class Message:
|
||||||
Create a new Message object with parameters, check for /me messages,
|
Create a new Message object with parameters, check for /me messages,
|
||||||
and delayed 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 '):
|
if txt.startswith('/me '):
|
||||||
me = True
|
me = True
|
||||||
txt = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_ME_MESSAGE),
|
txt = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_ME_MESSAGE),
|
||||||
txt[4:])
|
txt[4:])
|
||||||
else:
|
else:
|
||||||
me = False
|
me = False
|
||||||
str_time = time.strftime("%H:%M:%S")
|
self.history = history
|
||||||
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.nickname = nickname
|
||||||
|
self.nick_color = nick_color
|
||||||
self.user = user
|
self.user = user
|
||||||
self.identifier = identifier
|
|
||||||
self.top = top
|
self.top = top
|
||||||
self.highlight = highlight
|
self.highlight = highlight
|
||||||
self.me = me
|
self.me = me
|
||||||
|
@ -91,68 +136,29 @@ class Message:
|
||||||
rev -= 1
|
rev -= 1
|
||||||
return ''.join(acc)
|
return ''.join(acc)
|
||||||
|
|
||||||
def render(self, width: int, timestamp: bool = False, nick_size: int = 10) -> List["Line"]:
|
def compute_offset(self, with_timestamps: bool, nick_size: int) -> int:
|
||||||
"""
|
|
||||||
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
|
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:
|
if self.ack:
|
||||||
|
theme = get_theme()
|
||||||
if self.ack > 0:
|
if self.ack > 0:
|
||||||
offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1
|
offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1
|
||||||
else:
|
else:
|
||||||
offset += poopt.wcswidth(theme.CHAR_NACK) + 1
|
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:
|
if self.me:
|
||||||
offset += 1 # '* ' before and ' ' after
|
offset += 3
|
||||||
if timestamp:
|
else:
|
||||||
if self.str_time:
|
offset += 2
|
||||||
offset += 1 + len(self.str_time)
|
if self.revisions:
|
||||||
if theme.CHAR_TIME_LEFT and self.str_time:
|
offset += ceil(log10(self.revisions + 1))
|
||||||
offset += 1
|
return offset
|
||||||
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
|
|
||||||
|
|
|
@ -14,7 +14,8 @@ from poezio.ui.funcs import truncate_nick, parse_attrs
|
||||||
from poezio import poopt
|
from poezio import poopt
|
||||||
from poezio.config import config
|
from poezio.config import config
|
||||||
from poezio.theming import to_curses_attr, get_theme, dump_tuple
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -106,7 +107,7 @@ class BaseTextWin(Win):
|
||||||
Build a list of lines from a message, without adding it
|
Build a list of lines from a message, without adding it
|
||||||
to a list
|
to a list
|
||||||
"""
|
"""
|
||||||
return []
|
return build_lines(message, self.width, timestamp, nick_size)
|
||||||
|
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
@ -117,20 +118,6 @@ class BaseTextWin(Win):
|
||||||
"""
|
"""
|
||||||
self.addstr_colored(txt, y, x)
|
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)
|
|
||||||
self._win.attroff(curses_color)
|
|
||||||
self.addstr(' ')
|
|
||||||
return poopt.wcswidth(time) + 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# TODO: figure out the type of room.
|
# TODO: figure out the type of room.
|
||||||
def resize(self, height: int, width: int, y: int, x: int, room=None) -> None:
|
def resize(self, height: int, width: int, y: int, x: int, room=None) -> None:
|
||||||
if hasattr(self, 'width'):
|
if hasattr(self, 'width'):
|
||||||
|
@ -341,15 +328,6 @@ class TextWin(BaseTextWin):
|
||||||
self.built_lines.pop(0)
|
self.built_lines.pop(0)
|
||||||
return len(lines)
|
return len(lines)
|
||||||
|
|
||||||
def build_message(self, message: Optional[Message], timestamp: bool = False, nick_size: int = 10) -> List[Union[None, Line]]:
|
|
||||||
"""
|
|
||||||
Build a list of lines from a message, without adding it
|
|
||||||
to a list
|
|
||||||
"""
|
|
||||||
if message is None: # line separator
|
|
||||||
return [None]
|
|
||||||
return message.render(self.width, timestamp, nick_size)
|
|
||||||
|
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
log.debug('Refresh: %s', self.__class__.__name__)
|
log.debug('Refresh: %s', self.__class__.__name__)
|
||||||
if self.height <= 0:
|
if self.height <= 0:
|
||||||
|
@ -367,11 +345,10 @@ class TextWin(BaseTextWin):
|
||||||
if line:
|
if line:
|
||||||
msg = line.msg
|
msg = line.msg
|
||||||
if line.start_pos == 0:
|
if line.start_pos == 0:
|
||||||
offset = self.write_pre_msg(msg, with_timestamps,
|
offset = write_pre(msg, self, with_timestamps, nick_size)
|
||||||
nick_size)
|
|
||||||
elif y == 0:
|
elif y == 0:
|
||||||
offset = self.compute_offset(msg, with_timestamps,
|
offset = msg.compute_offset(with_timestamps,
|
||||||
nick_size)
|
nick_size)
|
||||||
self.write_text(
|
self.write_text(
|
||||||
y, offset,
|
y, offset,
|
||||||
line.prepend + line.msg.txt[line.start_pos:line.end_pos])
|
line.prepend + line.msg.txt[line.start_pos:line.end_pos])
|
||||||
|
@ -382,120 +359,11 @@ class TextWin(BaseTextWin):
|
||||||
self._win.attrset(0)
|
self._win.attrset(0)
|
||||||
self._refresh()
|
self._refresh()
|
||||||
|
|
||||||
def compute_offset(self, msg, with_timestamps, nick_size) -> int:
|
|
||||||
offset = 0
|
|
||||||
if with_timestamps and msg.str_time:
|
|
||||||
offset += poopt.wcswidth(msg.str_time) + 1
|
|
||||||
|
|
||||||
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.ack:
|
|
||||||
theme = get_theme()
|
|
||||||
if msg.ack > 0:
|
|
||||||
offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1
|
|
||||||
else:
|
|
||||||
offset += poopt.wcswidth(theme.CHAR_NACK) + 1
|
|
||||||
if msg.me:
|
|
||||||
offset += 3
|
|
||||||
else:
|
|
||||||
offset += 2
|
|
||||||
if msg.revisions:
|
|
||||||
offset += ceil(log10(msg.revisions + 1))
|
|
||||||
offset += self.write_revisions(msg)
|
|
||||||
return offset
|
|
||||||
|
|
||||||
def write_pre_msg(self, msg, with_timestamps, nick_size) -> int:
|
|
||||||
offset = 0
|
|
||||||
if with_timestamps:
|
|
||||||
offset += self.write_time(msg.str_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 += self.write_ack()
|
|
||||||
else:
|
|
||||||
offset += self.write_nack()
|
|
||||||
if msg.me:
|
|
||||||
self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE))
|
|
||||||
self.addstr('* ')
|
|
||||||
self.write_nickname(nick, color, msg.highlight)
|
|
||||||
offset += self.write_revisions(msg)
|
|
||||||
self.addstr(' ')
|
|
||||||
offset += 3
|
|
||||||
else:
|
|
||||||
self.write_nickname(nick, color, msg.highlight)
|
|
||||||
offset += self.write_revisions(msg)
|
|
||||||
self.addstr('> ')
|
|
||||||
offset += 2
|
|
||||||
return offset
|
|
||||||
|
|
||||||
def write_revisions(self, msg) -> int:
|
|
||||||
if msg.revisions:
|
|
||||||
self._win.attron(
|
|
||||||
to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE))
|
|
||||||
self.addstr('%d' % msg.revisions)
|
|
||||||
self._win.attrset(0)
|
|
||||||
return ceil(log10(msg.revisions + 1))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def write_line_separator(self, y) -> None:
|
def write_line_separator(self, y) -> None:
|
||||||
theme = get_theme()
|
theme = get_theme()
|
||||||
char = theme.CHAR_NEW_TEXT_SEPARATOR
|
char = theme.CHAR_NEW_TEXT_SEPARATOR
|
||||||
self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width,
|
self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width,
|
||||||
to_curses_attr(theme.COLOR_NEW_TEXT_SEPARATOR))
|
to_curses_attr(theme.COLOR_NEW_TEXT_SEPARATOR))
|
||||||
|
|
||||||
def write_ack(self) -> int:
|
|
||||||
theme = get_theme()
|
|
||||||
color = theme.COLOR_CHAR_ACK
|
|
||||||
self._win.attron(to_curses_attr(color))
|
|
||||||
self.addstr(theme.CHAR_ACK_RECEIVED)
|
|
||||||
self._win.attroff(to_curses_attr(color))
|
|
||||||
self.addstr(' ')
|
|
||||||
return poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1
|
|
||||||
|
|
||||||
def write_nack(self) -> int:
|
|
||||||
theme = get_theme()
|
|
||||||
color = theme.COLOR_CHAR_NACK
|
|
||||||
self._win.attron(to_curses_attr(color))
|
|
||||||
self.addstr(theme.CHAR_NACK)
|
|
||||||
self._win.attroff(to_curses_attr(color))
|
|
||||||
self.addstr(' ')
|
|
||||||
return poopt.wcswidth(theme.CHAR_NACK) + 1
|
|
||||||
|
|
||||||
def write_nickname(self, nickname, color, highlight=False) -> None:
|
|
||||||
"""
|
|
||||||
Write the nickname, using the user's color
|
|
||||||
and return the number of written characters
|
|
||||||
"""
|
|
||||||
if not nickname:
|
|
||||||
return
|
|
||||||
if highlight:
|
|
||||||
hl_color = get_theme().COLOR_HIGHLIGHT_NICK
|
|
||||||
if hl_color == "reverse":
|
|
||||||
self._win.attron(curses.A_REVERSE)
|
|
||||||
else:
|
|
||||||
color = hl_color
|
|
||||||
if color:
|
|
||||||
self._win.attron(to_curses_attr(color))
|
|
||||||
self.addstr(nickname)
|
|
||||||
if color:
|
|
||||||
self._win.attroff(to_curses_attr(color))
|
|
||||||
if highlight and hl_color == "reverse":
|
|
||||||
self._win.attroff(curses.A_REVERSE)
|
|
||||||
|
|
||||||
def modify_message(self, old_id, message) -> None:
|
def modify_message(self, old_id, message) -> None:
|
||||||
"""
|
"""
|
||||||
Find a message, and replace it with a new one
|
Find a message, and replace it with a new one
|
||||||
|
@ -544,28 +412,12 @@ class XMLTextWin(BaseTextWin):
|
||||||
if line:
|
if line:
|
||||||
msg = line.msg
|
msg = line.msg
|
||||||
if line.start_pos == 0:
|
if line.start_pos == 0:
|
||||||
if msg.nickname == theme.CHAR_XML_OUT:
|
offset = write_pre(msg, self, True, 10)
|
||||||
color = theme.COLOR_XML_OUT
|
|
||||||
elif msg.nickname == theme.CHAR_XML_IN:
|
|
||||||
color = theme.COLOR_XML_IN
|
|
||||||
self.write_time(msg.str_time)
|
|
||||||
self.write_prefix(msg.nickname, color)
|
|
||||||
self.addstr(' ')
|
|
||||||
if y != self.height - 1:
|
if y != self.height - 1:
|
||||||
self.addstr('\n')
|
self.addstr('\n')
|
||||||
self._win.attrset(0)
|
self._win.attrset(0)
|
||||||
for y, line in enumerate(lines):
|
for y, line in enumerate(lines):
|
||||||
offset = 0
|
offset = msg.compute_offset(True, 10)
|
||||||
# Offset for the timestamp (if any) plus a space after it
|
|
||||||
offset += len(line.msg.str_time)
|
|
||||||
# space
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
# Offset for the prefix
|
|
||||||
offset += poopt.wcswidth(truncate_nick(line.msg.nickname))
|
|
||||||
# space
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
self.write_text(
|
self.write_text(
|
||||||
y, offset,
|
y, offset,
|
||||||
line.prepend + line.msg.txt[line.start_pos:line.end_pos])
|
line.prepend + line.msg.txt[line.start_pos:line.end_pos])
|
||||||
|
@ -573,43 +425,3 @@ class XMLTextWin(BaseTextWin):
|
||||||
self.addstr('\n')
|
self.addstr('\n')
|
||||||
self._win.attrset(0)
|
self._win.attrset(0)
|
||||||
self._refresh()
|
self._refresh()
|
||||||
|
|
||||||
def build_message(self, message: Message, timestamp: bool = False, nick_size: int = 10) -> List[Line]:
|
|
||||||
txt = message.txt
|
|
||||||
ret = []
|
|
||||||
default_color = None
|
|
||||||
nick = truncate_nick(message.nickname, nick_size)
|
|
||||||
offset = 0
|
|
||||||
if nick:
|
|
||||||
offset += poopt.wcswidth(nick) + 1 # + nick + ' ' length
|
|
||||||
if message.str_time:
|
|
||||||
offset += 1 + len(message.str_time)
|
|
||||||
theme = get_theme()
|
|
||||||
if theme.CHAR_TIME_LEFT and message.str_time:
|
|
||||||
offset += 1
|
|
||||||
if theme.CHAR_TIME_RIGHT and message.str_time:
|
|
||||||
offset += 1
|
|
||||||
lines = poopt.cut_text(txt, self.width - offset - 1)
|
|
||||||
prepend = default_color if default_color else ''
|
|
||||||
attrs = [] # type: List[str]
|
|
||||||
for line in lines:
|
|
||||||
saved = Line(
|
|
||||||
msg=message,
|
|
||||||
start_pos=line[0],
|
|
||||||
end_pos=line[1],
|
|
||||||
prepend=prepend)
|
|
||||||
attrs = parse_attrs(message.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
|
|
||||||
|
|
||||||
def write_prefix(self, nickname, color) -> None:
|
|
||||||
self._win.attron(to_curses_attr(color))
|
|
||||||
self.addstr(truncate_nick(nickname))
|
|
||||||
self._win.attroff(to_curses_attr(color))
|
|
||||||
|
|
Loading…
Reference in a new issue