summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormathieui <mathieui@mathieui.net>2020-05-09 22:58:17 +0200
committermathieui <mathieui@mathieui.net>2020-05-09 22:58:17 +0200
commitf68fa1da5e2cccd396ee03eec12359ff905b7bc6 (patch)
tree135acd2d1ed2a4e7e382d9bd4f574ef6f54a023e
parentd22b4b8c218cbbaee62002d751bd69bfe1d1deab (diff)
parentca85411af06c59ec63b9cc2c0b2b2f398da79e8a (diff)
downloadpoezio-f68fa1da5e2cccd396ee03eec12359ff905b7bc6.tar.gz
poezio-f68fa1da5e2cccd396ee03eec12359ff905b7bc6.tar.bz2
poezio-f68fa1da5e2cccd396ee03eec12359ff905b7bc6.tar.xz
poezio-f68fa1da5e2cccd396ee03eec12359ff905b7bc6.zip
Merge branch 'split-message-rendering' into 'master'
split message rendering See merge request poezio/poezio!48
-rw-r--r--plugins/day_change.py9
-rw-r--r--plugins/embed.py13
-rw-r--r--plugins/lastlog.py16
-rw-r--r--plugins/otr.py92
-rw-r--r--poezio/core/core.py54
-rw-r--r--poezio/core/handlers.py159
-rw-r--r--poezio/mam.py21
-rw-r--r--poezio/tabs/basetabs.py77
-rw-r--r--poezio/tabs/conversationtab.py25
-rw-r--r--poezio/tabs/muctab.py213
-rw-r--r--poezio/tabs/privatetab.py100
-rw-r--r--poezio/tabs/rostertab.py5
-rw-r--r--poezio/tabs/xmltab.py7
-rw-r--r--poezio/text_buffer.py156
-rw-r--r--poezio/ui/__init__.py0
-rw-r--r--poezio/ui/consts.py14
-rw-r--r--poezio/ui/funcs.py (renamed from poezio/windows/funcs.py)14
-rw-r--r--poezio/ui/render.py253
-rw-r--r--poezio/ui/types.py191
-rw-r--r--poezio/user.py3
-rw-r--r--poezio/windows/__init__.py4
-rw-r--r--poezio/windows/base_wins.py18
-rw-r--r--poezio/windows/info_wins.py2
-rw-r--r--poezio/windows/inputs.py11
-rw-r--r--poezio/windows/text_win.py539
-rw-r--r--poezio/xhtml.py2
-rwxr-xr-xsetup.py2
-rw-r--r--test/test_ui/test_funcs.py46
-rw-r--r--test/test_ui/test_render.py145
-rw-r--r--test/test_ui/test_types.py126
30 files changed, 1363 insertions, 954 deletions
diff --git a/plugins/day_change.py b/plugins/day_change.py
index 051b447b..5d3ab37c 100644
--- a/plugins/day_change.py
+++ b/plugins/day_change.py
@@ -4,11 +4,12 @@ date has changed.
"""
+import datetime
from gettext import gettext as _
+
+from poezio import timed_events, tabs
from poezio.plugin import BasePlugin
-import datetime
-from poezio import tabs
-from poezio import timed_events
+from poezio.ui.types import InfoMessage
class Plugin(BasePlugin):
@@ -30,7 +31,7 @@ class Plugin(BasePlugin):
for tab in self.core.tabs:
if isinstance(tab, tabs.ChatTab):
- tab.add_message(msg)
+ tab.add_message(InfoMessage(msg))
self.core.refresh_window()
self.schedule_event()
diff --git a/plugins/embed.py b/plugins/embed.py
index 0c4a4a2a..aee7d44b 100644
--- a/plugins/embed.py
+++ b/plugins/embed.py
@@ -16,6 +16,7 @@ Usage
from poezio import tabs
from poezio.plugin import BasePlugin
from poezio.theming import get_theme
+from poezio.ui.types import Message
class Plugin(BasePlugin):
@@ -37,11 +38,13 @@ class Plugin(BasePlugin):
if not isinstance(tab, tabs.MucTab):
message['type'] = 'chat'
tab.add_message(
- message['body'],
- nickname=tab.core.own_nick,
- nick_color=get_theme().COLOR_OWN_NICK,
- identifier=message['id'],
- jid=tab.core.xmpp.boundjid,
+ Message(
+ message['body'],
+ nickname=tab.core.own_nick,
+ nick_color=get_theme().COLOR_OWN_NICK,
+ identifier=message['id'],
+ jid=tab.core.xmpp.boundjid,
+ ),
typ=1,
)
message.send()
diff --git a/plugins/lastlog.py b/plugins/lastlog.py
index 104399b4..49efa522 100644
--- a/plugins/lastlog.py
+++ b/plugins/lastlog.py
@@ -17,7 +17,8 @@ from datetime import datetime
from poezio.plugin import BasePlugin
from poezio import tabs
-from poezio.text_buffer import Message, TextBuffer
+from poezio.text_buffer import TextBuffer
+from poezio.ui.types import InfoMessage
def add_line(
@@ -26,18 +27,7 @@ def add_line(
datetime: Optional[datetime] = None,
) -> None:
"""Adds a textual entry in the TextBuffer"""
- text_buffer.add_message(
- text,
- datetime, # Time
- None, # Nickname
- None, # Nick Color
- False, # History
- None, # User
- False, # Highlight
- None, # Identifier
- None, # str_time
- None, # Jid
- )
+ text_buffer.add_message(InfoMessage(text, time=datetime))
class Plugin(BasePlugin):
diff --git a/plugins/otr.py b/plugins/otr.py
index 2ddc332b..81e1621f 100644
--- a/plugins/otr.py
+++ b/plugins/otr.py
@@ -205,6 +205,7 @@ from poezio.tabs import StaticConversationTab, PrivateTab
from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser
from poezio.core.structs import Completion
+from poezio.ui.types import InfoMessage, Message
POLICY_FLAGS = {
'ALLOW_V1': False,
@@ -385,25 +386,30 @@ class PoezioContext(Context):
log.debug('OTR conversation with %s refreshed', self.peer)
if self.getCurrentTrust():
msg = OTR_REFRESH_TRUSTED % format_dict
- tab.add_message(msg, typ=self.log)
+ tab.add_message(InfoMessage(msg), typ=self.log)
else:
msg = OTR_REFRESH_UNTRUSTED % format_dict
- tab.add_message(msg, typ=self.log)
+ tab.add_message(InfoMessage(msg), typ=self.log)
hl(tab)
elif newstate == STATE_FINISHED or newstate == STATE_PLAINTEXT:
log.debug('OTR conversation with %s finished', self.peer)
if tab:
- tab.add_message(OTR_END % format_dict, typ=self.log)
+ tab.add_message(InfoMessage(OTR_END % format_dict), typ=self.log)
hl(tab)
elif newstate == STATE_ENCRYPTED and tab:
if self.getCurrentTrust():
- tab.add_message(OTR_START_TRUSTED % format_dict, typ=self.log)
+ tab.add_message(InfoMessage(OTR_START_TRUSTED % format_dict), typ=self.log)
else:
format_dict['our_fpr'] = self.user.getPrivkey()
format_dict['remote_fpr'] = self.getCurrentKey()
- tab.add_message(OTR_TUTORIAL % format_dict, typ=0)
tab.add_message(
- OTR_START_UNTRUSTED % format_dict, typ=self.log)
+ InfoMessage(OTR_TUTORIAL % format_dict),
+ typ=0
+ )
+ tab.add_message(
+ InfoMessage(OTR_START_UNTRUSTED % format_dict),
+ typ=self.log,
+ )
hl(tab)
log.debug('Set encryption state of %s to %s', self.peer,
@@ -639,7 +645,7 @@ class Plugin(BasePlugin):
# Received an OTR error
proto_error = err.args[0].error # pylint: disable=no-member
format_dict['err'] = proto_error.decode('utf-8', errors='replace')
- tab.add_message(OTR_ERROR % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_ERROR % format_dict), typ=0)
del msg['body']
del msg['html']
hl(tab)
@@ -649,7 +655,7 @@ class Plugin(BasePlugin):
# Encrypted message received, but unreadable as we do not have
# an OTR session in place.
text = MESSAGE_UNREADABLE % format_dict
- tab.add_message(text, jid=msg['from'], typ=0)
+ tab.add_message(InfoMessage(text), typ=0)
hl(tab)
del msg['body']
del msg['html']
@@ -658,7 +664,7 @@ class Plugin(BasePlugin):
except crypt.InvalidParameterError:
# Malformed OTR payload and stuff
text = MESSAGE_INVALID % format_dict
- tab.add_message(text, jid=msg['from'], typ=0)
+ tab.add_message(InfoMessage(text), typ=0)
hl(tab)
del msg['body']
del msg['html']
@@ -669,7 +675,7 @@ class Plugin(BasePlugin):
import traceback
exc = traceback.format_exc()
format_dict['exc'] = exc
- tab.add_message(POTR_ERROR % format_dict, typ=0)
+ tab.add_message(InfoMessage(POTR_ERROR % format_dict), typ=0)
log.error('Unspecified error in the OTR plugin', exc_info=True)
return
# No error, proceed with the message
@@ -688,10 +694,10 @@ class Plugin(BasePlugin):
abort = get_tlv(tlvs, potr.proto.SMPABORTTLV)
if abort:
ctx.reset_smp()
- tab.add_message(SMP_ABORTED_PEER % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_ABORTED_PEER % format_dict), typ=0)
elif ctx.in_smp and not ctx.smpIsValid():
ctx.reset_smp()
- tab.add_message(SMP_ABORTED % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_ABORTED % format_dict), typ=0)
elif smp1 or smp1q:
# Received an SMP request (with a question or not)
if smp1q:
@@ -709,22 +715,22 @@ class Plugin(BasePlugin):
# we did not initiate it
ctx.smp_own = False
format_dict['q'] = question
- tab.add_message(SMP_REQUESTED % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_REQUESTED % format_dict), typ=0)
elif smp2:
# SMP reply received
if not ctx.in_smp:
ctx.reset_smp()
else:
- tab.add_message(SMP_PROGRESS % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_PROGRESS % format_dict), typ=0)
elif smp3 or smp4:
# Type 4 (SMP message 3) or 5 (SMP message 4) TLVs received
# in both cases it is the final message of the SMP exchange
if ctx.smpIsSuccess():
- tab.add_message(SMP_SUCCESS % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_SUCCESS % format_dict), typ=0)
if not ctx.getCurrentTrust():
- tab.add_message(SMP_RECIPROCATE % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_RECIPROCATE % format_dict), typ=0)
else:
- tab.add_message(SMP_FAIL % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_FAIL % format_dict), typ=0)
ctx.reset_smp()
hl(tab)
self.core.refresh_window()
@@ -780,12 +786,15 @@ class Plugin(BasePlugin):
if decode_newlines:
body = body.replace('<br/>', '\n').replace('<br>', '\n')
tab.add_message(
- body,
- nickname=tab.nick,
- jid=msg['from'],
- forced_user=user,
+ Message(
+ body,
+ nickname=tab.nick,
+ jid=msg['from'],
+ user=user,
+ nick_color=nick_color
+ ),
typ=ctx.log,
- nick_color=nick_color)
+ )
hl(tab)
self.core.refresh_window()
del msg['body']
@@ -826,19 +835,22 @@ class Plugin(BasePlugin):
tab.send_chat_state('inactive', always_send=True)
tab.add_message(
- msg['body'],
- nickname=self.core.own_nick or tab.own_nick,
- nick_color=get_theme().COLOR_OWN_NICK,
- identifier=msg['id'],
- jid=self.core.xmpp.boundjid,
- typ=ctx.log)
+ Message(
+ msg['body'],
+ nickname=self.core.own_nick or tab.own_nick,
+ nick_color=get_theme().COLOR_OWN_NICK,
+ identifier=msg['id'],
+ jid=self.core.xmpp.boundjid,
+ ),
+ typ=ctx.log
+ )
# remove everything from the message so that it doesn’t get sent
del msg['body']
del msg['replace']
del msg['html']
elif is_relevant(tab) and ctx and ctx.getPolicy('REQUIRE_ENCRYPTION'):
warning_msg = MESSAGE_NOT_SENT % format_dict
- tab.add_message(warning_msg, typ=0)
+ tab.add_message(InfoMessage(warning_msg), typ=0)
del msg['body']
del msg['replace']
del msg['html']
@@ -856,7 +868,7 @@ class Plugin(BasePlugin):
('\n - /message %s' % jid) for jid in res)
format_dict['help'] = help_msg
warning_msg = INCOMPATIBLE_TAB % format_dict
- tab.add_message(warning_msg, typ=0)
+ tab.add_message(InfoMessage(warning_msg), typ=0)
del msg['body']
del msg['replace']
del msg['html']
@@ -900,22 +912,22 @@ class Plugin(BasePlugin):
self.otr_start(tab, name, format_dict)
elif action == 'ourfpr':
format_dict['fpr'] = self.account.getPrivkey()
- tab.add_message(OTR_OWN_FPR % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_OWN_FPR % format_dict), typ=0)
elif action == 'fpr':
if name in self.contexts:
ctx = self.contexts[name]
if ctx.getCurrentKey() is not None:
format_dict['fpr'] = ctx.getCurrentKey()
- tab.add_message(OTR_REMOTE_FPR % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_REMOTE_FPR % format_dict), typ=0)
else:
- tab.add_message(OTR_NO_FPR % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_NO_FPR % format_dict), typ=0)
elif action == 'drop':
# drop the privkey (and obviously, end the current conversations before that)
for context in self.contexts.values():
if context.state not in (STATE_FINISHED, STATE_PLAINTEXT):
context.disconnect()
self.account.drop_privkey()
- tab.add_message(KEY_DROPPED % format_dict, typ=0)
+ tab.add_message(InfoMessage(KEY_DROPPED % format_dict), typ=0)
elif action == 'trust':
ctx = self.get_context(name)
key = ctx.getCurrentKey()
@@ -927,7 +939,7 @@ class Plugin(BasePlugin):
format_dict['key'] = key
ctx.setTrust(fpr, 'verified')
self.account.saveTrusts()
- tab.add_message(TRUST_ADDED % format_dict, typ=0)
+ tab.add_message(InfoMessage(TRUST_ADDED % format_dict), typ=0)
elif action == 'untrust':
ctx = self.get_context(name)
key = ctx.getCurrentKey()
@@ -939,7 +951,7 @@ class Plugin(BasePlugin):
format_dict['key'] = key
ctx.setTrust(fpr, '')
self.account.saveTrusts()
- tab.add_message(TRUST_REMOVED % format_dict, typ=0)
+ tab.add_message(InfoMessage(TRUST_REMOVED % format_dict), typ=0)
self.core.refresh_window()
def otr_start(self, tab, name, format_dict):
@@ -954,7 +966,7 @@ class Plugin(BasePlugin):
if otr.state != STATE_ENCRYPTED:
format_dict['secs'] = secs
text = OTR_NOT_ENABLED % format_dict
- tab.add_message(text, typ=0)
+ tab.add_message(InfoMessage(text), typ=0)
self.core.refresh_window()
if secs > 0:
@@ -962,7 +974,7 @@ class Plugin(BasePlugin):
self.api.add_timed_event(event)
body = self.get_context(name).sendMessage(0, b'?OTRv?').decode()
self.core.xmpp.send_message(mto=name, mtype='chat', mbody=body)
- tab.add_message(OTR_REQUEST % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_REQUEST % format_dict), typ=0)
@staticmethod
def completion_otr(the_input):
@@ -1012,13 +1024,13 @@ class Plugin(BasePlugin):
ctx.smpInit(secret, question)
else:
ctx.smpInit(secret)
- tab.add_message(SMP_INITIATED % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_INITIATED % format_dict), typ=0)
elif action == 'answer':
ctx.smpGotSecret(secret)
elif action == 'abort':
if ctx.in_smp:
ctx.smpAbort()
- tab.add_message(SMP_ABORTED % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_ABORTED % format_dict), typ=0)
self.core.refresh_window()
@staticmethod
diff --git a/poezio/core/core.py b/poezio/core/core.py
index 14852ac2..8f25d551 100644
--- a/poezio/core/core.py
+++ b/poezio/core/core.py
@@ -53,8 +53,15 @@ from poezio.core.completions import CompletionCore
from poezio.core.tabs import Tabs
from poezio.core.commands import CommandCore
from poezio.core.handlers import HandlerCore
-from poezio.core.structs import POSSIBLE_SHOW, DEPRECATED_ERRORS, \
- ERROR_AND_STATUS_CODES, Command, Status
+from poezio.core.structs import (
+ Command,
+ Status,
+ DEPRECATED_ERRORS,
+ ERROR_AND_STATUS_CODES,
+ POSSIBLE_SHOW,
+)
+
+from poezio.ui.types import Message, InfoMessage
log = logging.getLogger(__name__)
@@ -1317,7 +1324,7 @@ class Core:
"""
tab = self.tabs.by_name_and_class(jid, tabs.ConversationTab)
if tab is not None:
- tab.add_message(msg, typ=2)
+ tab.add_message(InfoMessage(msg), typ=2)
if self.tabs.current_tab is tab:
self.refresh_window()
@@ -1349,7 +1356,12 @@ 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)
+ Message(
+ 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()
@@ -1579,17 +1591,6 @@ class Core:
self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0)
self.left_tab_win = None
- def add_message_to_text_buffer(self, buff, txt, nickname=None):
- """
- Add the message to the room if possible, else, add it to the Info window
- (in the Info tab of the info window in the RosterTab)
- """
- if not buff:
- self.information('Trying to add a message in no room: %s' % txt,
- 'Error')
- return
- buff.add_message(txt, nickname=nickname)
-
def full_screen_redraw(self):
"""
Completely erase and redraw the screen
@@ -2061,15 +2062,18 @@ class Core:
return
error_message = self.get_error_message(error)
tab.add_message(
- error_message,
- highlight=True,
- nickname='Error',
- nick_color=get_theme().COLOR_ERROR_MSG,
- typ=2)
+ Message(
+ error_message,
+ highlight=True,
+ nickname='Error',
+ nick_color=get_theme().COLOR_ERROR_MSG,
+ ),
+ typ=2,
+ )
code = error['error']['code']
if code == '401':
msg = 'To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)'
- tab.add_message(msg, typ=2)
+ tab.add_message(InfoMessage(msg), typ=2)
if code == '409':
if config.get('alternative_nickname') != '':
if not tab.joined:
@@ -2078,8 +2082,12 @@ class Core:
else:
if not tab.joined:
tab.add_message(
- 'You can join the room with an other nick, by typing "/join /other_nick"',
- typ=2)
+ InfoMessage(
+ 'You can join the room with another nick, '
+ 'by typing "/join /other_nick"'
+ ),
+ typ=2,
+ )
self.refresh_window()
diff --git a/poezio/core/handlers.py b/poezio/core/handlers.py
index a68b6986..3e2a20a0 100644
--- a/poezio/core/handlers.py
+++ b/poezio/core/handlers.py
@@ -39,6 +39,7 @@ from poezio.logger import logger
from poezio.roster import roster
from poezio.text_buffer import CorrectionError, AckError
from poezio.theming import dump_tuple, get_theme
+from poezio.ui.types import XMLLog, Message as PMessage, BaseMessage, InfoMessage
from poezio.core.commands import dumb_callback
@@ -326,7 +327,7 @@ class HandlerCore:
error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK),
error_msg)
if not tab.nack_message('\n' + error, message['id'], message['to']):
- tab.add_message(error, typ=0)
+ tab.add_message(InfoMessage(error), typ=0)
self.core.refresh_window()
def on_normal_message(self, message):
@@ -421,14 +422,17 @@ class HandlerCore:
if not try_modify():
conversation.add_message(
- body,
- date,
- nickname=remote_nick,
- nick_color=color,
- history=delayed,
- identifier=message['id'],
- jid=jid,
- typ=1)
+ PMessage(
+ txt=body,
+ time=date,
+ nickname=remote_nick,
+ nick_color=color,
+ history=delayed,
+ identifier=message['id'],
+ jid=jid,
+ ),
+ typ=1,
+ )
if not own and 'private' in config.get('beep_on').split():
if not config.get_by_tabname('disable_beep', conv_jid.bare):
@@ -750,6 +754,11 @@ class HandlerCore:
old_state = tab.state
delayed, date = common.find_delayed_tag(message)
+
+ history = (tab.last_message_was_history is None and delayed) or \
+ (tab.last_message_was_history and delayed)
+ tab.last_message_was_history = history
+
replaced = False
if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
replaced_id = message['replace']['id']
@@ -762,6 +771,7 @@ class HandlerCore:
replaced_id,
message['id'],
time=delayed_date,
+ delayed=delayed,
nickname=nick_from,
user=user):
self.core.events.trigger('highlight', message, tab)
@@ -769,12 +779,16 @@ class HandlerCore:
except CorrectionError:
log.debug('Unable to correct a message', exc_info=True)
if not replaced and tab.add_message(
- body,
- date,
- nick_from,
- history=delayed,
- identifier=message['id'],
- jid=message['from'],
+ PMessage(
+ txt=body,
+ time=date,
+ nickname=nick_from,
+ history=history,
+ delayed=delayed,
+ identifier=message['id'],
+ jid=message['from'],
+ user=user,
+ ),
typ=1):
self.core.events.trigger('highlight', message, tab)
@@ -862,14 +876,16 @@ class HandlerCore:
log.debug('Unable to correct a message', exc_info=True)
if not replaced:
tab.add_message(
- body,
- time=None,
- nickname=sender_nick,
- nick_color=get_theme().COLOR_OWN_NICK if sent else None,
- forced_user=user,
- identifier=message['id'],
- jid=message['from'],
- typ=1)
+ PMessage(
+ txt=body,
+ nickname=sender_nick,
+ nick_color=get_theme().COLOR_OWN_NICK if sent else None,
+ user=user,
+ identifier=message['id'],
+ jid=message['from'],
+ ),
+ typ=1,
+ )
if sent:
tab.set_last_sent_message(message, correct=replaced)
else:
@@ -1361,36 +1377,52 @@ class HandlerCore:
if show_unavailable or hide_unavailable or non_priv or logging_off\
or non_anon or semi_anon or full_anon:
tab.add_message(
- '\x19%(info_col)s}Info: A configuration change not privacy-related occurred.' % info_col,
+ InfoMessage(
+ 'Info: A configuration change not privacy-related occurred.'
+ ),
typ=2)
modif = True
if show_unavailable:
tab.add_message(
- '\x19%(info_col)s}Info: The unavailable members are now shown.' % info_col,
+ InfoMessage(
+ 'Info: The unavailable members are now shown.'
+ ),
typ=2)
elif hide_unavailable:
tab.add_message(
- '\x19%(info_col)s}Info: The unavailable members are now hidden.' % info_col,
+ InfoMessage(
+ 'Info: The unavailable members are now hidden.',
+ ),
typ=2)
if non_anon:
tab.add_message(
- '\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % info_col,
+ InfoMessage(
+ '\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % info_col
+ ),
typ=2)
elif semi_anon:
tab.add_message(
- '\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % info_col,
+ InfoMessage(
+ 'Info: The room is now semi-anonymous. (moderators-only JID)',
+ ),
typ=2)
elif full_anon:
tab.add_message(
- '\x19%(info_col)s}Info: The room is now fully anonymous.' % info_col,
+ InfoMessage(
+ 'Info: The room is now fully anonymous.',
+ ),
typ=2)
if logging_on:
tab.add_message(
- '\x191}Warning: \x19%(info_col)s}This room is publicly logged' % info_col,
+ InfoMessage(
+ '\x191}Warning: \x19%(info_col)s}This room is publicly logged' % info_col
+ ),
typ=2)
elif logging_off:
tab.add_message(
- '\x19%(info_col)s}Info: This room is not logged anymore.' % info_col,
+ InfoMessage(
+ 'Info: This room is not logged anymore.',
+ ),
typ=2)
if modif:
self.core.refresh_window()
@@ -1434,15 +1466,17 @@ class HandlerCore:
if nick_from:
tab.add_message(
- "%(user)s set the subject to: \x19%(text_col)s}%(subject)s"
- % fmt,
- str_time=time,
+ InfoMessage(
+ "%(user)s set the subject to: \x19%(text_col)s}%(subject)s" % fmt,
+ time=time,
+ ),
typ=2)
else:
tab.add_message(
- "\x19%(info_col)s}The subject is: \x19%(text_col)s}%(subject)s"
- % fmt,
- str_time=time,
+ InfoMessage(
+ "The subject is: \x19%(text_col)s}%(subject)s" % fmt,
+ time=time,
+ ),
typ=2)
tab.topic = subject
tab.topic_from = nick_from
@@ -1484,17 +1518,16 @@ class HandlerCore:
"""
jid_from = message['from']
self.core.information('%s requests your attention!' % jid_from, 'Info')
- for tab in self.core.tabs:
- if tab.jid == jid_from:
- tab.state = 'attention'
- self.core.refresh_tab_win()
- return
- for tab in self.core.tabs:
- if tab.jid.bare == jid_from.bare:
- tab.state = 'attention'
- self.core.refresh_tab_win()
- return
- self.core.information('%s tab not found.' % jid_from, 'Error')
+ tab = (
+ self.core.tabs.by_name_and_class(
+ jid_from.full, tabs.ChatTab
+ ) or self.core.tabs.by_name_and_class(
+ jid_from.bare, tabs.ChatTab
+ )
+ )
+ if tab and tab is not self.core.tabs.current_tab:
+ tab.state = "attention"
+ self.core.refresh_tab_win()
def outgoing_stanza(self, stanza):
"""
@@ -1507,18 +1540,15 @@ class HandlerCore:
xhtml_text, force=True).rstrip('\x19o').strip()
else:
poezio_colored = str(stanza)
- char = get_theme().CHAR_XML_OUT
- self.core.add_message_to_text_buffer(
- self.core.xml_buffer,
- poezio_colored,
- nickname=char)
+ self.core.xml_buffer.add_message(
+ XMLLog(txt=poezio_colored, incoming=False),
+ )
try:
if self.core.xml_tab.match_stanza(
ElementBase(ET.fromstring(stanza))):
- self.core.add_message_to_text_buffer(
- self.core.xml_tab.filtered_buffer,
- poezio_colored,
- nickname=char)
+ self.core.xml_tab.filtered_buffer.add_message(
+ XMLLog(txt=poezio_colored, incoming=False),
+ )
except:
# Most of the time what gets logged is whitespace pings. Skip.
# And also skip tab updates.
@@ -1541,17 +1571,14 @@ class HandlerCore:
xhtml_text, force=True).rstrip('\x19o').strip()
else:
poezio_colored = str(stanza)
- char = get_theme().CHAR_XML_IN
- self.core.add_message_to_text_buffer(
- self.core.xml_buffer,
- poezio_colored,
- nickname=char)
+ self.core.xml_buffer.add_message(
+ XMLLog(txt=poezio_colored, incoming=True),
+ )
try:
if self.core.xml_tab.match_stanza(stanza):
- self.core.add_message_to_text_buffer(
- self.core.xml_tab.filtered_buffer,
- poezio_colored,
- nickname=char)
+ self.core.xml_tab.filtered_buffer.add_message(
+ XMLLog(txt=poezio_colored, incoming=True),
+ )
except:
log.debug('', exc_info=True)
if isinstance(self.core.tabs.current_tab, tabs.XMLTab):
diff --git a/poezio/mam.py b/poezio/mam.py
index 0f745f30..50dad4a3 100644
--- a/poezio/mam.py
+++ b/poezio/mam.py
@@ -18,6 +18,7 @@ from poezio import tabs
from poezio import xhtml, colors
from poezio.config import config
from poezio.text_buffer import TextBuffer
+from poezio.ui.types import Message
class DiscoInfoException(Exception): pass
@@ -63,17 +64,15 @@ def add_line(
nick = nick.split('/')[0]
color = get_theme().COLOR_OWN_NICK
text_buffer.add_message(
- txt=text,
- time=time,
- nickname=nick,
- nick_color=color,
- history=True,
- user=None,
- highlight=False,
- top=top,
- identifier=None,
- str_time=None,
- jid=None,
+ Message(
+ txt=text,
+ time=time,
+ nickname=nick,
+ nick_color=color,
+ history=True,
+ user=None,
+ top=top,
+ )
)
diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py
index fca54860..fbb0c4cf 100644
--- a/poezio/tabs/basetabs.py
+++ b/poezio/tabs/basetabs.py
@@ -31,7 +31,13 @@ from typing import (
TYPE_CHECKING,
)
-from poezio import mam, poopt, timed_events, xhtml, windows
+from poezio import (
+ mam,
+ poopt,
+ timed_events,
+ xhtml,
+ windows
+)
from poezio.core.structs import Command, Completion, Status
from poezio.common import safeJID
from poezio.config import config
@@ -39,7 +45,9 @@ 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 poezio.ui.types import BaseMessage, InfoMessage
from slixmpp import JID, InvalidJID, Message
@@ -565,40 +573,19 @@ class ChatTab(Tab):
def general_jid(self) -> JID:
raise NotImplementedError
- def log_message(self,
- txt: str,
- nickname: str,
- time: Optional[datetime] = None,
- typ=1):
+ def log_message(self, message: BaseMessage, typ=1):
"""
Log the messages in the archives.
"""
name = self.jid.bare
- if not logger.log_message(name, nickname, txt, date=time, typ=typ):
+ if not isinstance(message, Message):
+ return
+ if not logger.log_message(name, message.nickname, message.txt, date=message.time, typ=typ):
self.core.information('Unable to write in the log file', 'Error')
- def add_message(self,
- txt,
- time=None,
- nickname=None,
- forced_user=None,
- nick_color=None,
- identifier=None,
- jid=None,
- history=None,
- typ=1,
- highlight=False):
- self.log_message(txt, nickname, time=time, typ=typ)
- self._text_buffer.add_message(
- txt,
- time=time,
- nickname=nickname,
- highlight=highlight,
- nick_color=nick_color,
- history=history,
- user=forced_user,
- identifier=identifier,
- jid=jid)
+ def add_message(self, message: BaseMessage, typ=1):
+ self.log_message(message, typ=typ)
+ self._text_buffer.add_message(message)
def modify_message(self,
txt,
@@ -607,10 +594,10 @@ class ChatTab(Tab):
user=None,
jid=None,
nickname=None):
- self.log_message(txt, nickname, typ=1)
message = self._text_buffer.modify_message(
- txt, old_id, new_id, time=time, user=user, jid=jid)
+ txt, old_id, new_id, user=user, jid=jid)
if message:
+ self.log_message(message, typ=1)
self.text_win.modify_message(message.identifier, message)
self.core.refresh_window()
return True
@@ -833,12 +820,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)
@@ -1007,7 +990,10 @@ class OneToOneTab(ChatTab):
msg += 'status: %s, ' % status.message
if status.show in SHOW_NAME:
msg += 'show: %s, ' % SHOW_NAME[status.show]
- self.add_message(msg[:-2], typ=2)
+ self.add_message(
+ InfoMessage(txt=msg[:-2]),
+ typ=2,
+ )
def ack_message(self, msg_id: str, msg_jid: JID):
"""
@@ -1039,11 +1025,14 @@ class OneToOneTab(ChatTab):
message.send()
body = xhtml.xhtml_to_poezio_colors(xhtml_data, force=True)
self._text_buffer.add_message(
- body,
- nickname=self.core.own_nick,
- nick_color=get_theme().COLOR_OWN_NICK,
- identifier=message['id'],
- jid=self.core.xmpp.boundjid)
+ Message(
+ body,
+ nickname=self.core.own_nick,
+ nick_color=get_theme().COLOR_OWN_NICK,
+ identifier=message['id'],
+ jid=self.core.xmpp.boundjid,
+ )
+ )
self.refresh()
def check_features(self):
diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py
index 410c5eda..70005f0f 100644
--- a/poezio/tabs/conversationtab.py
+++ b/poezio/tabs/conversationtab.py
@@ -28,6 +28,7 @@ from poezio.roster import roster
from poezio.text_buffer import CorrectionError
from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser
+from poezio.ui.types import InfoMessage
log = logging.getLogger(__name__)
@@ -179,7 +180,7 @@ class ConversationTab(OneToOneTab):
(' and their last status was %s' % status)
if status else '',
)
- self.add_message(msg)
+ self.add_message(InfoMessage(msg), typ=0)
self.core.refresh_window()
self.core.xmpp.plugin['xep_0012'].get_last_activity(
@@ -200,17 +201,21 @@ class ConversationTab(OneToOneTab):
if resource:
status = (
'Status: %s' % resource.status) if resource.status else ''
- self._text_buffer.add_message(
- "\x19%(info_col)s}Show: %(show)s, %(status)s\x19o" % {
- 'show': resource.presence or 'available',
- 'status': status,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
- })
+ self.add_message(
+ InfoMessage(
+ "Show: %(show)s, %(status)s" % {
+ 'show': resource.presence or 'available',
+ 'status': status,
+ }
+ ),
+ typ=0,
+ )
return True
else:
- self._text_buffer.add_message(
- "\x19%(info_col)s}No information available\x19o" %
- {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
+ self.add_message(
+ InfoMessage("No information available"),
+ typ=0,
+ )
return True
@command_args_parser.quoted(0, 1)
diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py
index 73359610..f0c055b1 100644
--- a/poezio/tabs/muctab.py
+++ b/poezio/tabs/muctab.py
@@ -40,6 +40,7 @@ from poezio.roster import roster
from poezio.theming import get_theme, dump_tuple
from poezio.user import User
from poezio.core.structs import Completion, Status
+from poezio.ui.types import BaseMessage, Message, InfoMessage, StatusMessage
log = logging.getLogger(__name__)
@@ -75,6 +76,8 @@ class MucTab(ChatTab):
self.users = [] # type: List[User]
# private conversations
self.privates = [] # type: List[Tab]
+ # Used to check if we are still receiving muc history
+ self.last_message_was_history = None # type: Optional[bool]
self.topic = ''
self.topic_from = ''
# Self ping event, so we can cancel it when we leave the room
@@ -197,8 +200,7 @@ class MucTab(ChatTab):
'color_spec': spec_col,
'nick': self.own_nick,
}
-
- self.add_message(msg, typ=2)
+ self.add_message(InfoMessage(msg), typ=2)
self.disconnect()
muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
message)
@@ -301,7 +303,7 @@ class MucTab(ChatTab):
'role': user.role or 'None',
'status': '\n%s' % user.status if user.status else ''
}
- self.add_message(info, typ=0)
+ self.add_message(InfoMessage(info), typ=0)
return True
def change_topic(self, topic: str):
@@ -327,9 +329,13 @@ class MucTab(ChatTab):
else:
user_string = ''
- self._text_buffer.add_message(
- "\x19%s}The subject of the room is: \x19%s}%s %s" %
- (info_text, norm_text, self.topic, user_string))
+ self.add_message(
+ InfoMessage(
+ "The subject of the room is: \x19%s}%s %s" %
+ (norm_text, self.topic, user_string),
+ ),
+ typ=0,
+ )
@refresh_wrapper.always
def recolor(self, random_colors=False):
@@ -558,28 +564,32 @@ class MucTab(ChatTab):
'nick_col': color,
'info_col': info_col,
}
- self.add_message(enable_message, typ=2)
+ self.add_message(InfoMessage(enable_message), typ=2)
self.core.enable_private_tabs(self.jid.bare, enable_message)
if '201' in status_codes:
self.add_message(
- '\x19%(info_col)s}Info: The room '
- 'has been created' % {'info_col': info_col},
- typ=0)
+ InfoMessage('Info: The room has been created'),
+ typ=0
+ )
if '170' in status_codes:
self.add_message(
- '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
- ' This room is publicly logged' % {
- 'info_col': info_col,
- 'warn_col': warn_col
- },
+ InfoMessage(
+ '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
+ ' This room is publicly logged' % {
+ 'info_col': info_col,
+ 'warn_col': warn_col
+ },
+ ),
typ=0)
if '100' in status_codes:
self.add_message(
- '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
- ' This room is not anonymous.' % {
- 'info_col': info_col,
- 'warn_col': warn_col
- },
+ InfoMessage(
+ '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
+ ' This room is not anonymous.' % {
+ 'info_col': info_col,
+ 'warn_col': warn_col
+ },
+ ),
typ=0)
def handle_presence_joined(self, presence, status_codes):
@@ -635,18 +645,20 @@ class MucTab(ChatTab):
def on_non_member_kicked(self):
"""We have been kicked because the MUC is members-only"""
self.add_message(
- '\x19%(info_col)s}You have been kicked because you '
- 'are not a member and the room is now members-only.' %
- {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ InfoMessage(
+ 'You have been kicked because you '
+ 'are not a member and the room is now members-only.'
+ ),
typ=2)
self.disconnect()
def on_muc_shutdown(self):
"""We have been kicked because the MUC service is shutting down"""
self.add_message(
- '\x19%(info_col)s}You have been kicked because the'
- ' MUC service is shutting down.' %
- {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ InfoMessage(
+ 'You have been kicked because the'
+ ' MUC service is shutting down.'
+ ),
typ=2)
self.disconnect()
@@ -693,7 +705,7 @@ class MucTab(ChatTab):
'jid_color': dump_tuple(theme.COLOR_MUC_JID),
'color_spec': spec_col,
}
- self.add_message(msg, typ=2)
+ self.add_message(InfoMessage(msg), typ=2)
self.core.on_user_rejoined_private_conversation(self.jid.bare, from_nick)
def on_user_nick_change(self, presence, user, from_nick, from_room):
@@ -723,14 +735,16 @@ class MucTab(ChatTab):
old_color = color = 3
info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
self.add_message(
- '\x19%(old_color)s}%(old)s\x19%(info_col)s} is'
- ' now known as \x19%(color)s}%(new)s' % {
- 'old': from_nick,
- 'new': new_nick,
- 'color': color,
- 'old_color': old_color,
- 'info_col': info_col
- },
+ InfoMessage(
+ '\x19%(old_color)s}%(old)s\x19%(info_col)s} is'
+ ' now known as \x19%(color)s}%(new)s' % {
+ 'old': from_nick,
+ 'new': new_nick,
+ 'color': color,
+ 'old_color': old_color,
+ 'info_col': info_col
+ },
+ ),
typ=2)
# rename the private tabs if needed
self.core.rename_private_tabs(self.jid.bare, from_nick, user)
@@ -814,7 +828,7 @@ class MucTab(ChatTab):
'reason': reason.text,
'info_col': info_col
}
- self.add_message(kick_msg, typ=2)
+ self.add_message(InfoMessage(kick_msg), typ=2)
def on_user_kicked(self, presence, user, from_nick):
"""
@@ -892,7 +906,7 @@ class MucTab(ChatTab):
'reason': reason.text,
'info_col': info_col
}
- self.add_message(kick_msg, typ=2)
+ self.add_message(InfoMessage(kick_msg), typ=2)
def on_user_leave_groupchat(self,
user: User,
@@ -957,7 +971,7 @@ class MucTab(ChatTab):
}
if status:
leave_msg += ' (\x19o%s\x19%s})' % (status, info_col)
- self.add_message(leave_msg, typ=2)
+ self.add_message(InfoMessage(leave_msg), typ=2)
self.core.on_user_left_private_conversation(from_room, user, status)
def on_user_change_status(self, user, from_nick, from_room, affiliation,
@@ -1016,7 +1030,7 @@ class MucTab(ChatTab):
or show != user.show or status != user.status)) or (
affiliation != user.affiliation or role != user.role):
# display the message in the room
- self._text_buffer.add_message(msg)
+ self.add_message(InfoMessage(msg))
self.core.on_user_changed_status_in_private(
'%s/%s' % (from_room, from_nick), Status(show, status))
self.users.remove(user)
@@ -1042,13 +1056,15 @@ class MucTab(ChatTab):
"""
return self.topic.replace('\n', '|')
- def log_message(self, txt, nickname, time=None, typ=1):
+ def log_message(self, msg: Message, typ=1):
"""
Log the messages in the archives, if it needs
to be
"""
- if time is None and self.joined: # don't log the history messages
- if not logger.log_message(self.jid.bare, nickname, txt, typ=typ):
+ if not isinstance(msg, Message):
+ return
+ if not msg.history and self.joined: # don't log the history messages
+ if not logger.log_message(self.jid.bare, msg.nickname, msg.txt, typ=typ):
self.core.information('Unable to write in the log file',
'Error')
@@ -1061,65 +1077,37 @@ class MucTab(ChatTab):
return user
return None
- def add_message(self, txt, time=None, nickname=None, **kwargs):
+ def add_message(self, msg: BaseMessage, typ=1):
"""
Note that user can be None even if nickname is not None. It happens
when we receive an history message said by someone who is not
in the room anymore
Return True if the message highlighted us. False otherwise.
"""
-
# reset self-ping interval
if self.self_ping_event:
self.enable_self_ping_event()
-
- self.log_message(txt, nickname, time=time, typ=kwargs.get('typ', 1))
- args = dict()
- for key, value in kwargs.items():
- if key not in ('typ', 'forced_user'):
- args[key] = value
- if nickname is not None:
- user = self.get_user_by_name(nickname)
- else:
- user = None
-
- if user:
- user.set_last_talked(datetime.now())
- args['user'] = user
- if not user and kwargs.get('forced_user'):
- args['user'] = kwargs['forced_user']
-
- if (not time and nickname and nickname != self.own_nick
- and self.state != 'current'):
- if (self.state != 'highlight'
- and config.get_by_tabname('notify_messages', self.jid.bare)):
+ super().add_message(msg, typ=typ)
+ if not isinstance(msg, Message):
+ return
+ if msg.user:
+ msg.user.set_last_talked(msg.time)
+ if config.get_by_tabname('notify_messages', self.jid.bare) and self.state != 'current':
+ if msg.nickname != self.own_nick and not msg.history:
self.state = 'message'
- if time and not txt.startswith('/me'):
- txt = '\x19%(info_col)s}%(txt)s' % {
- 'txt': txt,
- 'info_col': dump_tuple(get_theme().COLOR_LOG_MSG)
- }
- elif not nickname:
- txt = '\x19%(info_col)s}%(txt)s' % {
- 'txt': txt,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
- }
- elif not kwargs.get('highlight'): # TODO
- args['highlight'] = self.do_highlight(txt, time, nickname)
- time = time or datetime.now()
- self._text_buffer.add_message(txt, time, nickname, **args)
- return args.get('highlight', False)
+ msg.highlight = self.do_highlight(msg.txt, msg.nickname, msg.delayed)
+ return msg.highlight
def modify_message(self,
txt,
old_id,
new_id,
time=None,
+ delayed: bool = False,
nickname=None,
user=None,
jid=None):
- self.log_message(txt, nickname, time=time, typ=1)
- highlight = self.do_highlight(txt, time, nickname, corrected=True)
+ highlight = self.do_highlight(txt, nickname, delayed, corrected=True)
message = self._text_buffer.modify_message(
txt,
old_id,
@@ -1129,6 +1117,7 @@ class MucTab(ChatTab):
user=user,
jid=jid)
if message:
+ self.log_message(message, typ=1)
self.text_win.modify_message(message.identifier, message)
return highlight
return False
@@ -1192,9 +1181,11 @@ class MucTab(ChatTab):
def on_self_ping_failed(self, iq):
if not self.lagged:
self.lagged = True
- info_text = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
self._text_buffer.add_message(
- "\x19%s}MUC service not responding." % info_text)
+ InfoMessage(
+ "MUC service not responding."
+ ),
+ )
self._state = 'disconnected'
self.core.refresh_window()
self.enable_self_ping_event()
@@ -1202,9 +1193,9 @@ class MucTab(ChatTab):
def reset_lag(self):
if self.lagged:
self.lagged = False
- info_text = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
- self._text_buffer.add_message(
- "\x19%s}MUC service is responding again." % info_text)
+ self.add_message(
+ InfoMessage("MUC service is responding again.")
+ )
if self != self.core.tabs.current_tab:
self._state = 'joined'
else:
@@ -1323,28 +1314,38 @@ class MucTab(ChatTab):
def build_highlight_regex(self, nickname):
return re.compile(r"(^|\W)" + re.escape(nickname) + r"(\W|$)", re.I)
- def is_highlight(self, txt, time, nickname, own_nick, highlight_on,
- corrected=False):
+ def is_highlight(self, txt: str, nick: str, highlight_on: List[str],
+ delayed, corrected: bool = False):
+ """
+ Highlight algorithm for MUC tabs
+ """
+
highlighted = False
- if (not time or corrected) and nickname and nickname != own_nick:
- if self.build_highlight_regex(own_nick).search(txt):
+ if not delayed and not corrected:
+ if self.build_highlight_regex(nick).search(txt):
highlighted = True
else:
- highlight_words = highlight_on.split(':')
- for word in highlight_words:
+ for word in highlight_on:
if word and word.lower() in txt.lower():
highlighted = True
break
return highlighted
- def do_highlight(self, txt, time, nickname, corrected=False):
+ def do_highlight(self, txt, nickname, delayed, corrected=False):
"""
Set the tab color and returns the nick color
"""
own_nick = self.own_nick
- highlight_on = config.get_by_tabname('highlight_on', self.general_jid)
- highlighted = self.is_highlight(txt, time, nickname, own_nick,
- highlight_on, corrected)
+ highlight_on = config.get_by_tabname(
+ 'highlight_on',
+ self.general_jid,
+ ).split(':')
+
+ # Don't highlight on info message or our own messages
+ if not nickname or nickname == own_nick:
+ return False
+
+ highlighted = self.is_highlight(txt, own_nick, highlight_on, delayed, corrected)
if highlighted and self.joined:
if self.state != 'current':
self.state = 'highlight'
@@ -1551,7 +1552,7 @@ class MucTab(ChatTab):
buff.append('\n')
message = ' '.join(buff)
- self._text_buffer.add_message(message)
+ self.add_message(InfoMessage(message), typ=0)
self.text_win.refresh()
self.input.refresh()
@@ -1650,16 +1651,18 @@ class MucTab(ChatTab):
if all_errors:
self.add_message(
- 'Can\'t access affiliations',
- highlight=True,
- nickname='Error',
- nick_color=theme.COLOR_ERROR_MSG,
+ Message(
+ 'Can\'t access affiliations',
+ highlight=True,
+ nickname='Error',
+ nick_color=theme.COLOR_ERROR_MSG,
+ ),
typ=2,
)
self.core.refresh_window()
return None
- self._text_buffer.add_message('Affiliations')
+ self._text_buffer.add_message(InfoMessage('Affiliations'))
for iq in iqs:
if isinstance(iq, (IqError, IqTimeout)):
continue
@@ -1672,12 +1675,12 @@ class MucTab(ChatTab):
affiliation = items[0].get('affiliation')
aff_char = aff_colors[affiliation]
self._text_buffer.add_message(
- ' %s%s' % (aff_char, affiliation.capitalize()),
+ InfoMessage(' %s%s' % (aff_char, affiliation.capitalize())),
)
items = map(lambda i: i.get('jid'), items)
for ajid in sorted(items):
- self._text_buffer.add_message(' %s' % ajid)
+ self._text_buffer.add_message(InfoMessage(' %s' % ajid))
self.core.refresh_window()
return None
diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py
index 7206240f..b43294a1 100644
--- a/poezio/tabs/privatetab.py
+++ b/poezio/tabs/privatetab.py
@@ -27,6 +27,7 @@ from poezio.decorators import refresh_wrapper
from poezio.logger import logger
from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser
+from poezio.ui.types import BaseMessage, Message, InfoMessage
log = logging.getLogger(__name__)
@@ -106,12 +107,14 @@ class PrivateTab(OneToOneTab):
def remove_information_element(plugin_name):
del PrivateTab.additional_information[plugin_name]
- def log_message(self, txt, nickname, time=None, typ=1):
+ def log_message(self, msg: BaseMessage, typ=1):
"""
Log the messages in the archives.
"""
+ if not isinstance(msg, Message):
+ return
if not logger.log_message(
- self.jid.full, nickname, txt, date=time, typ=typ):
+ self.jid.full, msg.nickname, msg.txt, date=msg.time, typ=typ):
self.core.information('Unable to write in the log file', 'Error')
def on_close(self):
@@ -163,8 +166,6 @@ class PrivateTab(OneToOneTab):
self.core.events.trigger('private_say', msg, self)
if not msg['body']:
return
- user = self.parent_muc.get_user_by_name(self.own_nick)
- replaced = False
if correct or msg['replace']['id']:
msg['replace']['id'] = self.last_sent_message['id']
else:
@@ -311,13 +312,15 @@ class PrivateTab(OneToOneTab):
display a message.
"""
self.add_message(
- '\x19%(nick_col)s}%(old)s\x19%(info_col)s} is now '
- 'known as \x19%(nick_col)s}%(new)s' % {
- 'old': old_nick,
- 'new': user.nick,
- 'nick_col': dump_tuple(user.color),
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
- },
+ InfoMessage(
+ '\x19%(nick_col)s}%(old)s\x19%(info_col)s} is now '
+ 'known as \x19%(nick_col)s}%(new)s' % {
+ 'old': old_nick,
+ 'new': user.nick,
+ 'nick_col': dump_tuple(user.color),
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
+ },
+ ),
typ=2)
new_jid = self.jid.bare + '/' + user.nick
self.name = new_jid
@@ -338,27 +341,31 @@ class PrivateTab(OneToOneTab):
if not status_message:
self.add_message(
- '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}'
- '%(nick)s\x19%(info_col)s} has left the room' % {
- 'nick': user.nick,
- 'spec': theme.CHAR_QUIT,
- 'nick_col': color,
- 'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR),
- 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
- },
+ InfoMessage(
+ '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}'
+ '%(nick)s\x19%(info_col)s} has left the room' % {
+ 'nick': user.nick,
+ 'spec': theme.CHAR_QUIT,
+ 'nick_col': color,
+ 'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR),
+ 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
+ },
+ ),
typ=2)
else:
self.add_message(
- '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}'
- '%(nick)s\x19%(info_col)s} has left the room'
- ' (%(status)s)' % {
- 'status': status_message,
- 'nick': user.nick,
- 'spec': theme.CHAR_QUIT,
- 'nick_col': color,
- 'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR),
- 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
- },
+ InfoMessage(
+ '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}'
+ '%(nick)s\x19%(info_col)s} has left the room'
+ ' (%(status)s)' % {
+ 'status': status_message,
+ 'nick': user.nick,
+ 'spec': theme.CHAR_QUIT,
+ 'nick_col': color,
+ 'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR),
+ 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
+ },
+ ),
typ=2)
return self.core.tabs.current_tab is self
@@ -378,26 +385,28 @@ class PrivateTab(OneToOneTab):
if user:
color = dump_tuple(user.color)
self.add_message(
- '\x19%(join_col)s}%(spec)s \x19%(color)s}%(nick)s\x19'
- '%(info_col)s} joined the room' % {
- 'nick': nick,
- 'color': color,
- 'spec': theme.CHAR_JOIN,
- 'join_col': dump_tuple(theme.COLOR_JOIN_CHAR),
- 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
- },
+ InfoMessage(
+ '\x19%(join_col)s}%(spec)s \x19%(color)s}%(nick)s\x19'
+ '%(info_col)s} joined the room' % {
+ 'nick': nick,
+ 'color': color,
+ 'spec': theme.CHAR_JOIN,
+ 'join_col': dump_tuple(theme.COLOR_JOIN_CHAR),
+ 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
+ },
+ ),
typ=2)
return self.core.tabs.current_tab is self
def activate(self, reason=None):
self.on = True
if reason:
- self.add_message(txt=reason, typ=2)
+ self.add_message(InfoMessage(reason), typ=2)
def deactivate(self, reason=None):
self.on = False
if reason:
- self.add_message(txt=reason, typ=2)
+ self.add_message(InfoMessage(reason), typ=2)
def matching_names(self):
return [(3, self.jid.resource), (4, self.name)]
@@ -407,9 +416,12 @@ class PrivateTab(OneToOneTab):
error = '\x19%s}%s\x19o' % (dump_tuple(theme.COLOR_CHAR_NACK),
error_message)
self.add_message(
- error,
- highlight=True,
- nickname='Error',
- nick_color=theme.COLOR_ERROR_MSG,
- typ=2)
+ Message(
+ error,
+ highlight=True,
+ nickname='Error',
+ nick_color=theme.COLOR_ERROR_MSG,
+ ),
+ typ=2,
+ )
self.core.refresh_window()
diff --git a/poezio/tabs/rostertab.py b/poezio/tabs/rostertab.py
index 6f43cca1..50b8c0d5 100644
--- a/poezio/tabs/rostertab.py
+++ b/poezio/tabs/rostertab.py
@@ -27,6 +27,7 @@ from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser
from poezio.core.structs import Command, Completion
from poezio.tabs import Tab
+from poezio.ui.types import InfoMessage
log = logging.getLogger(__name__)
@@ -402,11 +403,11 @@ class RosterInfoTab(Tab):
if not tab:
log.debug('Received message from nonexistent tab: %s',
message['from'])
- message = '\x19%(info_col)s}Cannot send message to %(jid)s: contact blocked' % {
+ message = 'Cannot send message to %(jid)s: contact blocked' % {
'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
'jid': message['from'],
}
- tab.add_message(message)
+ tab.add_message(InfoMessage(message), typ=0)
@command_args_parser.ignored
def command_list_blocks(self):
diff --git a/poezio/tabs/xmltab.py b/poezio/tabs/xmltab.py
index c4a50df8..2611d9d5 100644
--- a/poezio/tabs/xmltab.py
+++ b/poezio/tabs/xmltab.py
@@ -65,7 +65,7 @@ class XMLTab(Tab):
self.filtered_buffer = text_buffer.TextBuffer()
self.info_header = windows.XMLInfoWin()
- self.text_win = windows.XMLTextWin()
+ self.text_win = windows.TextWin()
self.core_buffer.add_window(self.text_win)
self.default_help_message = windows.HelpText("/ to enter a command")
@@ -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'),
+ 'IN' if msg.incoming else 'OUT',
+ clean_text(msg.txt))
for msg in xml))
filename = os.path.expandvars(os.path.expanduser(args[0]))
try:
diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py
index 2c0d192a..3b3ac051 100644
--- a/poezio/text_buffer.py
+++ b/poezio/text_buffer.py
@@ -11,93 +11,20 @@ independently by their TextWins.
import logging
log = logging.getLogger(__name__)
-from typing import Dict, Union, Optional, List, Tuple
+from typing import (
+ Dict,
+ List,
+ Optional,
+ TYPE_CHECKING,
+ Tuple,
+ Union,
+)
from datetime import datetime
from poezio.config import config
-from poezio.theming import get_theme, dump_tuple
-
-
-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)
+from poezio.ui.types import Message, BaseMessage
+
+if TYPE_CHECKING:
+ from poezio.windows.text_win import TextWin
class CorrectionError(Exception):
@@ -120,50 +47,25 @@ class TextBuffer:
messages_nb_limit = config.get('max_messages_in_memory')
self._messages_nb_limit = messages_nb_limit # type: int
# Message objects
- self.messages = [] # type: List[Message]
+ self.messages = [] # type: List[BaseMessage]
# COMPAT: Correction id -> Original message id.
self.correction_ids = {} # type: Dict[str, str]
# we keep track of one or more windows
# so we can pass the new messages to them, as they are added, so
# they (the windows) can build the lines from the new message
- self._windows = []
+ self._windows = [] # type: List[TextWin]
def add_window(self, win) -> None:
self._windows.append(win)
@property
- def last_message(self) -> Optional[Message]:
+ def last_message(self) -> Optional[BaseMessage]:
return self.messages[-1] if self.messages else None
- def add_message(self,
- txt: str,
- time: Optional[datetime] = None,
- nickname: Optional[str] = None,
- nick_color: Optional[Tuple] = None,
- history: bool = False,
- user: Optional[str] = None,
- highlight: bool = False,
- top: Optional[bool] = False,
- identifier: Optional[str] = None,
- str_time: Optional[str] = None,
- jid: Optional[str] = None,
- ack: int = 0) -> int:
+ def add_message(self, msg: BaseMessage):
"""
Create a message and add it to the text buffer
"""
- msg = Message(
- txt,
- time,
- nickname,
- nick_color,
- history,
- user,
- identifier,
- top,
- str_time=str_time,
- highlight=highlight,
- jid=jid,
- ack=ack)
self.messages.append(msg)
while len(self.messages) > self._messages_nb_limit:
@@ -176,13 +78,11 @@ class TextBuffer:
# build the lines from the new message
nb = window.build_new_message(
msg,
- history=history,
- highlight=highlight,
timestamp=show_timestamps,
- top=top,
nick_size=nick_size)
if ret_val == 0:
ret_val = nb
+ top = isinstance(msg, Message) and msg.top
if window.pos != 0 and top is False:
window.scroll_up(nb)
@@ -222,6 +122,8 @@ class TextBuffer:
if i == -1:
return None
msg = self.messages[i]
+ if not isinstance(msg, Message):
+ return None
if msg.ack == 1: # Message was already acked
return False
if msg.jid != jid:
@@ -240,7 +142,7 @@ class TextBuffer:
highlight: bool = False,
time: Optional[datetime] = None,
user: Optional[str] = None,
- jid: Optional[str] = None):
+ jid: Optional[str] = None) -> Message:
"""
Correct a message in a text buffer.
@@ -259,10 +161,11 @@ class TextBuffer:
raise CorrectionError("nothing to replace")
msg = self.messages[i]
-
+ if not isinstance(msg, Message):
+ raise CorrectionError('Wrong message type')
if msg.user and msg.user is not user:
raise CorrectionError("Different users")
- elif len(msg.str_time) > 8: # ugly
+ elif msg.delayed:
raise CorrectionError("Delayed message")
elif not msg.user and (msg.jid is None or jid is None):
raise CorrectionError('Could not check the '
@@ -277,13 +180,12 @@ 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,
+ user=msg.user,
+ identifier=orig_id,
highlight=highlight,
old_message=msg,
revisions=msg.revisions + 1,
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..0838d953
--- /dev/null
+++ b/poezio/ui/consts.py
@@ -0,0 +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/windows/funcs.py b/poezio/ui/funcs.py
index 22977374..023432ee 100644
--- a/poezio/windows/funcs.py
+++ b/poezio/ui/funcs.py
@@ -4,14 +4,14 @@ Standalone functions used by the modules
import string
from typing import Optional, List
-from poezio.windows.base_wins import FORMAT_CHAR, format_chars
+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
+ to_find = chars or FORMAT_CHARS
pos = -1
for char in to_find:
p = text.find(char)
@@ -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..b8368312
--- /dev/null
+++ b/poezio/ui/render.py
@@ -0,0 +1,253 @@
+import logging
+import curses
+
+from datetime import datetime
+from functools import singledispatch
+from math import ceil, log10
+from typing import (
+ List,
+ Tuple,
+ TYPE_CHECKING,
+)
+
+from poezio import poopt
+from poezio.theming import (
+ get_theme,
+)
+from poezio.ui.consts import (
+ FORMAT_CHAR,
+ LONG_FORMAT,
+ SHORT_FORMAT,
+)
+from poezio.ui.funcs import (
+ truncate_nick,
+ parse_attrs,
+)
+from poezio.ui.types import (
+ BaseMessage,
+ Message,
+ StatusMessage,
+ 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 = [] # 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)
+ generated_lines = generate_lines(lines, msg, default_color='')
+ if msg.top:
+ generated_lines.reverse()
+ 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(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
+ """
+ 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: '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:
+ 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
new file mode 100644
index 00000000..ae72b6b9
--- /dev/null
+++ b/poezio/ui/types.py
@@ -0,0 +1,191 @@
+
+from datetime import datetime
+from math import ceil, log10
+from typing import Union, Optional, List, Tuple
+from poezio.ui.funcs import truncate_nick
+from poezio import poopt
+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 InfoMessage(BaseMessage):
+ def __init__(self, txt: str, identifier: str = '', time: Optional[datetime] = None):
+ txt = ('\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT)) + txt
+ super().__init__(txt=txt, identifier=identifier, time=time)
+
+
+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()
+ if with_timestamps:
+ offset += 1 + SHORT_FORMAT_LENGTH
+ if self.incoming:
+ nick = theme.CHAR_XML_IN
+ else:
+ nick = theme.CHAR_XML_OUT
+ nick = truncate_nick(nick, nick_size) or ''
+ offset += 1 + len(nick)
+ return offset
+
+
+class StatusMessage(BaseMessage):
+ __slots__ = ('txt', 'time', 'identifier', 'format_string', 'format_args')
+
+ def __init__(self, format_string: str, format_args: dict):
+ BaseMessage.__init__(
+ self,
+ txt='',
+ )
+ self.format_string = format_string
+ self.format_args = format_args
+ self.rebuild()
+
+ def rebuild(self):
+ real_args = {}
+ for key, func in self.format_args.items():
+ real_args[key] = func()
+ self.txt = self.format_string.format(**real_args)
+
+
+class Message(BaseMessage):
+ __slots__ = ('txt', 'nick_color', 'time', 'nickname', 'user', 'delayed', 'history',
+ 'identifier', 'top', 'highlight', 'me', 'old_message', 'revisions',
+ 'jid', 'ack')
+
+ def __init__(self,
+ txt: str,
+ nickname: Optional[str],
+ time: Optional[datetime] = None,
+ nick_color: Optional[Tuple] = None,
+ delayed: bool = False,
+ history: bool = False,
+ user: Optional[User] = None,
+ identifier: Optional[str] = '',
+ top: Optional[bool] = False,
+ 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
+ """
+ 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
+ self.txt = txt
+ self.delayed = delayed or history
+ self.history = history
+ self.nickname = nickname
+ self.nick_color = nick_color
+ self.user = user
+ 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 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 self.me:
+ offset += 3
+ else:
+ offset += 2
+ if self.revisions:
+ offset += ceil(log10(self.revisions + 1))
+ return offset
diff --git a/poezio/user.py b/poezio/user.py
index 146a70da..bead0a93 100644
--- a/poezio/user.py
+++ b/poezio/user.py
@@ -97,7 +97,8 @@ class User:
"""
time: datetime object
"""
- self.last_talked = time
+ if time > self.last_talked:
+ self.last_talked = time
def has_talked_since(self, t: int) -> bool:
"""
diff --git a/poezio/windows/__init__.py b/poezio/windows/__init__.py
index 8775b0a2..bbd6dc30 100644
--- a/poezio/windows/__init__.py
+++ b/poezio/windows/__init__.py
@@ -17,7 +17,7 @@ from poezio.windows.list import ListWin, ColumnHeaderWin
from poezio.windows.misc import VerticalSeparator
from poezio.windows.muc import UserList, Topic
from poezio.windows.roster_win import RosterWin, ContactInfoWin
-from poezio.windows.text_win import BaseTextWin, TextWin, XMLTextWin
+from poezio.windows.text_win import TextWin
from poezio.windows.image import ImageWin
__all__ = [
@@ -28,5 +28,5 @@ __all__ = [
'BookmarksInfoWin', 'ConfirmStatusWin', 'HelpText', 'Input',
'HistoryInput', 'MessageInput', 'CommandInput', 'ListWin',
'ColumnHeaderWin', 'VerticalSeparator', 'UserList', 'Topic', 'RosterWin',
- 'ContactInfoWin', 'TextWin', 'XMLTextWin', 'ImageWin', 'BaseTextWin'
+ 'ContactInfoWin', 'TextWin', 'ImageWin'
]
diff --git a/poezio/windows/base_wins.py b/poezio/windows/base_wins.py
index ac6b4804..d6c72912 100644
--- a/poezio/windows/base_wins.py
+++ b/poezio/windows/base_wins.py
@@ -20,10 +20,7 @@ from typing import Optional, Tuple, TYPE_CHECKING
from poezio.theming import to_curses_attr, read_tuple
-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'
+from poezio.ui.consts import FORMAT_CHAR
if TYPE_CHECKING:
from _curses import _CursesWindow # pylint: disable=E0611
@@ -80,6 +77,19 @@ class Win:
# of the screen.
pass
+ @contextmanager
+ def colored_text(self, color: Optional[Tuple]=None, attr: Optional[int]=None):
+ """Context manager which sets up an attr/color when inside"""
+ if attr is None:
+ if color is not None:
+ attr = to_curses_attr(color)
+ else:
+ yield None
+ return
+ self._win.attron(attr)
+ yield None
+ self._win.attroff(attr)
+
def addstr(self, *args) -> None:
"""
Safe call to addstr
diff --git a/poezio/windows/info_wins.py b/poezio/windows/info_wins.py
index 3a8d1863..d31130fe 100644
--- a/poezio/windows/info_wins.py
+++ b/poezio/windows/info_wins.py
@@ -10,7 +10,7 @@ from poezio.common import safeJID
from poezio.config import config
from poezio.windows.base_wins import Win
-from poezio.windows.funcs import truncate_nick
+from poezio.ui.funcs import truncate_nick
from poezio.theming import get_theme, to_curses_attr
diff --git a/poezio/windows/inputs.py b/poezio/windows/inputs.py
index 84b95599..5cca8803 100644
--- a/poezio/windows/inputs.py
+++ b/poezio/windows/inputs.py
@@ -10,8 +10,9 @@ from typing import List, Dict, Callable, Optional
from poezio import keyboard
from poezio import common
from poezio import poopt
-from poezio.windows.base_wins import Win, format_chars
-from poezio.windows.funcs import find_first_format_char
+from poezio.windows.base_wins import Win
+from poezio.ui.consts import FORMAT_CHARS
+from poezio.ui.funcs import find_first_format_char
from poezio.config import config
from poezio.theming import to_curses_attr
@@ -487,7 +488,7 @@ class Input(Win):
(\x0E to \x19 instead of \x19 + attr). We do not use any }
char in this version
"""
- chars = format_chars + '\n'
+ chars = FORMAT_CHARS + '\n'
if y is not None and x is not None:
self.move(y, x)
format_char = find_first_format_char(text, chars)
@@ -497,7 +498,7 @@ class Input(Win):
if text[format_char] == '\n':
attr_char = '|'
else:
- attr_char = self.text_attributes[format_chars.index(
+ attr_char = self.text_attributes[FORMAT_CHARS.index(
text[format_char])]
self.addstr(text[:format_char])
self.addstr(attr_char, curses.A_REVERSE)
@@ -696,7 +697,7 @@ class MessageInput(HistoryInput):
def cb(attr_char):
if attr_char in self.text_attributes:
- char = format_chars[self.text_attributes.index(attr_char)]
+ char = FORMAT_CHARS[self.text_attributes.index(attr_char)]
self.do_command(char, False)
self.rewrite_text()
diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py
index f4c78c2c..a1b12ae9 100644
--- a/poezio/windows/text_win.py
+++ b/poezio/windows/text_win.py
@@ -9,36 +9,27 @@ from math import ceil, log10
from typing import Optional, List, Union
from poezio.windows.base_wins import Win, FORMAT_CHAR
-from poezio.windows.funcs import truncate_nick, parse_attrs
+from poezio.ui.funcs import truncate_nick, parse_attrs
+from poezio.text_buffer import TextBuffer
from poezio import poopt
from poezio.config import config
from poezio.theming import to_curses_attr, get_theme, dump_tuple
-from poezio.text_buffer import Message
+from poezio.ui.types import Message, BaseMessage
+from poezio.ui.render import Line, build_lines, write_pre
log = logging.getLogger(__name__)
-# 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
-
-
-class BaseTextWin(Win):
+class TextWin(Win):
__slots__ = ('lines_nb_limit', 'pos', 'built_lines', 'lock', 'lock_buffer',
- 'separator_after')
+ 'separator_after', 'highlights', 'hl_pos',
+ 'nb_of_highlights_after_separator')
def __init__(self, lines_nb_limit: Optional[int] = None) -> None:
+ Win.__init__(self)
if lines_nb_limit is None:
lines_nb_limit = config.get('max_lines_in_memory')
- Win.__init__(self)
self.lines_nb_limit = lines_nb_limit # type: int
self.pos = 0
# Each new message is built and kept here.
@@ -48,6 +39,17 @@ class BaseTextWin(Win):
self.lock = False
self.lock_buffer = [] # type: List[Union[None, Line]]
self.separator_after = None # type: Optional[Line]
+ # the Lines of the highlights in that buffer
+ self.highlights = [] # type: List[Line]
+ # the current HL position in that list NaN means that we’re not on
+ # an hl. -1 is a valid position (it's before the first hl of the
+ # list. i.e the separator, in the case where there’s no hl before
+ # it.)
+ self.hl_pos = float('nan')
+
+ # Keep track of the number of hl after the separator.
+ # This is useful to make “go to next highlight“ work after a “move to separator”.
+ self.nb_of_highlights_after_separator = 0
def toggle_lock(self) -> bool:
if self.lock:
@@ -80,25 +82,21 @@ class BaseTextWin(Win):
self.pos = 0
return self.pos != pos
- # TODO: figure out the type of history.
def build_new_message(self,
- message: Message,
- history=None,
+ message: BaseMessage,
clean: bool = True,
highlight: bool = False,
timestamp: bool = False,
- top: Optional[bool] = False,
nick_size: int = 10) -> int:
"""
Take one message, build it and add it to the list
Return the number of lines that are built for the given
message.
"""
- #pylint: disable=assignment-from-no-return
- lines = self.build_message(
- message, timestamp=timestamp, nick_size=nick_size)
- if top:
- lines.reverse()
+ lines = build_lines(
+ message, self.width, timestamp=timestamp, nick_size=nick_size
+ )
+ if isinstance(message, Message) and message.top:
for line in lines:
self.built_lines.insert(0, line)
else:
@@ -108,20 +106,46 @@ class BaseTextWin(Win):
self.built_lines.extend(lines)
if not lines or not lines[0]:
return 0
+ if highlight:
+ self.highlights.append(lines[0])
+ self.nb_of_highlights_after_separator += 1
+ log.debug("Number of highlights after separator is now %s",
+ self.nb_of_highlights_after_separator)
if clean:
while len(self.built_lines) > self.lines_nb_limit:
self.built_lines.pop(0)
return len(lines)
- def build_message(self, message: 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
- """
- return []
-
def refresh(self) -> None:
- pass
+ log.debug('Refresh: %s', self.__class__.__name__)
+ if self.height <= 0:
+ return
+ if self.pos == 0:
+ lines = self.built_lines[-self.height:]
+ else:
+ lines = self.built_lines[-self.height - self.pos:-self.pos]
+ with_timestamps = config.get("show_timestamps")
+ nick_size = config.get("max_nick_length")
+ self._win.move(0, 0)
+ self._win.erase()
+ offset = 0
+ for y, line in enumerate(lines):
+ if line:
+ msg = line.msg
+ if line.start_pos == 0:
+ offset = write_pre(msg, self, with_timestamps, nick_size)
+ elif y == 0:
+ offset = msg.compute_offset(with_timestamps,
+ nick_size)
+ self.write_text(
+ y, offset,
+ line.prepend + line.msg.txt[line.start_pos:line.end_pos])
+ else:
+ self.write_line_separator(y)
+ if y != self.height - 1:
+ self.addstr('\n')
+ self._win.attrset(0)
+ self._refresh()
def write_text(self, y: int, x: int, txt: str) -> None:
"""
@@ -129,22 +153,7 @@ 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)
- self._win.attroff(curses_color)
- self.addstr(' ')
- return poopt.wcswidth(time) + 1
- return 0
-
- # 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: TextBuffer=None) -> None:
if hasattr(self, 'width'):
old_width = self.width
else:
@@ -161,8 +170,7 @@ class BaseTextWin(Win):
if self.pos < 0:
self.pos = 0
- # TODO: figure out the type of room.
- def rebuild_everything(self, room) -> None:
+ def rebuild_everything(self, room: TextBuffer) -> None:
self.built_lines = []
with_timestamps = config.get('show_timestamps')
nick_size = config.get('max_nick_length')
@@ -170,37 +178,46 @@ class BaseTextWin(Win):
self.build_new_message(
message,
clean=False,
- top=message.top,
timestamp=with_timestamps,
nick_size=nick_size)
if self.separator_after is message:
- self.build_new_message(None)
+ self.built_lines.append(None)
while len(self.built_lines) > self.lines_nb_limit:
self.built_lines.pop(0)
- def __del__(self) -> None:
- log.debug('** TextWin: deleting %s built lines',
- (len(self.built_lines)))
- del self.built_lines
-
+ def remove_line_separator(self) -> None:
+ """
+ Remove the line separator
+ """
+ log.debug('remove_line_separator')
+ if None in self.built_lines:
+ self.built_lines.remove(None)
+ self.separator_after = None
-class TextWin(BaseTextWin):
- __slots__ = ('highlights', 'hl_pos', 'nb_of_highlights_after_separator')
+ def add_line_separator(self, room: TextBuffer = None) -> None:
+ """
+ add a line separator at the end of messages list
+ room is a textbuffer that is needed to get the previous message
+ (in case of resize)
+ """
+ if None not in self.built_lines:
+ self.built_lines.append(None)
+ self.nb_of_highlights_after_separator = 0
+ log.debug("Resetting number of highlights after separator")
+ if room and room.messages:
+ self.separator_after = room.messages[-1]
- def __init__(self, lines_nb_limit: Optional[int] = None) -> None:
- BaseTextWin.__init__(self, lines_nb_limit)
- # the Lines of the highlights in that buffer
- self.highlights = [] # type: List[Line]
- # the current HL position in that list NaN means that we’re not on
- # an hl. -1 is a valid position (it's before the first hl of the
- # list. i.e the separator, in the case where there’s no hl before
- # it.)
- self.hl_pos = float('nan')
+ def write_line_separator(self, y) -> None:
+ theme = get_theme()
+ char = theme.CHAR_NEW_TEXT_SEPARATOR
+ self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width,
+ to_curses_attr(theme.COLOR_NEW_TEXT_SEPARATOR))
- # Keep track of the number of hl after the separator.
- # This is useful to make “go to next highlight“ work after a “move to separator”.
- self.nb_of_highlights_after_separator = 0
+ def __del__(self) -> None:
+ log.debug('** TextWin: deleting %s built lines',
+ (len(self.built_lines)))
+ del self.built_lines
def next_highlight(self) -> None:
"""
@@ -293,269 +310,6 @@ class TextWin(BaseTextWin):
self.highlights) - self.nb_of_highlights_after_separator - 1
log.debug("self.hl_pos = %s", self.hl_pos)
- def remove_line_separator(self) -> None:
- """
- Remove the line separator
- """
- log.debug('remove_line_separator')
- if None in self.built_lines:
- self.built_lines.remove(None)
- self.separator_after = None
-
- # TODO: figure out the type of room.
- def add_line_separator(self, room=None) -> None:
- """
- add a line separator at the end of messages list
- room is a textbuffer that is needed to get the previous message
- (in case of resize)
- """
- if None not in self.built_lines:
- self.built_lines.append(None)
- self.nb_of_highlights_after_separator = 0
- log.debug("Resetting number of highlights after separator")
- if room and room.messages:
- self.separator_after = room.messages[-1]
-
- # TODO: figure out the type of history.
- def build_new_message(self,
- message: Message,
- history: bool = False,
- clean: bool = True,
- highlight: bool = False,
- timestamp: bool = False,
- top: Optional[bool] = False,
- nick_size: int = 10) -> int:
- """
- Take one message, build it and add it to the list
- Return the number of lines that are built for the given
- message.
- """
- lines = self.build_message(
- message, timestamp=timestamp, nick_size=nick_size)
- if top:
- lines.reverse()
- for line in lines:
- self.built_lines.insert(0, line)
- else:
- if self.lock:
- self.lock_buffer.extend(lines)
- else:
- self.built_lines.extend(lines)
- if not lines or not lines[0]:
- return 0
- if highlight:
- self.highlights.append(lines[0])
- self.nb_of_highlights_after_separator += 1
- log.debug("Number of highlights after separator is now %s",
- self.nb_of_highlights_after_separator)
- if clean:
- while len(self.built_lines) > self.lines_nb_limit:
- self.built_lines.pop(0)
- 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]
- txt = message.txt
- if not txt:
- return []
- theme = get_theme()
- if len(message.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(message.nickname, nick_size)
- offset = 0
- if message.ack:
- if message.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 message.revisions > 0:
- offset += ceil(log10(message.revisions + 1))
- if message.me:
- offset += 1 # '* ' before and ' ' after
- 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
- 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 refresh(self) -> None:
- log.debug('Refresh: %s', self.__class__.__name__)
- if self.height <= 0:
- return
- if self.pos == 0:
- lines = self.built_lines[-self.height:]
- else:
- lines = self.built_lines[-self.height - self.pos:-self.pos]
- with_timestamps = config.get("show_timestamps")
- nick_size = config.get("max_nick_length")
- self._win.move(0, 0)
- self._win.erase()
- offset = 0
- for y, line in enumerate(lines):
- if line:
- msg = line.msg
- if line.start_pos == 0:
- offset = self.write_pre_msg(msg, with_timestamps,
- nick_size)
- elif y == 0:
- offset = self.compute_offset(msg, with_timestamps,
- nick_size)
- self.write_text(
- y, offset,
- line.prepend + line.msg.txt[line.start_pos:line.end_pos])
- else:
- self.write_line_separator(y)
- if y != self.height - 1:
- self.addstr('\n')
- self._win.attrset(0)
- 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:
- theme = get_theme()
- char = theme.CHAR_NEW_TEXT_SEPARATOR
- self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width,
- 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:
"""
Find a message, and replace it with a new one
@@ -564,112 +318,23 @@ class TextWin(BaseTextWin):
with_timestamps = config.get('show_timestamps')
nick_size = config.get('max_nick_length')
for i in range(len(self.built_lines) - 1, -1, -1):
- if self.built_lines[i] and self.built_lines[i].msg.identifier == old_id:
+ current = self.built_lines[i]
+ if current is not None and current.msg.identifier == old_id:
index = i
- while index >= 0 and self.built_lines[index] and self.built_lines[index].msg.identifier == old_id:
+ while (
+ index >= 0
+ and current is not None
+ and current.msg.identifier == old_id
+ ):
self.built_lines.pop(index)
index -= 1
+ if index >= 0:
+ current = self.built_lines[index]
index += 1
- lines = self.build_message(
- message, timestamp=with_timestamps, nick_size=nick_size)
+ lines = build_lines(
+ message, self.width, timestamp=with_timestamps, nick_size=nick_size
+ )
for line in lines:
self.built_lines.insert(index, line)
index += 1
break
-
- def __del__(self) -> None:
- log.debug('** TextWin: deleting %s built lines',
- (len(self.built_lines)))
- del self.built_lines
-
-
-class XMLTextWin(BaseTextWin):
- __slots__ = ()
-
- def __init__(self) -> None:
- BaseTextWin.__init__(self)
-
- def refresh(self) -> None:
- log.debug('Refresh: %s', self.__class__.__name__)
- theme = get_theme()
- if self.height <= 0:
- return
- if self.pos == 0:
- lines = self.built_lines[-self.height:]
- else:
- lines = self.built_lines[-self.height - self.pos:-self.pos]
- self._win.move(0, 0)
- self._win.erase()
- for y, line in enumerate(lines):
- if line:
- msg = line.msg
- if line.start_pos == 0:
- if msg.nickname == theme.CHAR_XML_OUT:
- 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:
- self.addstr('\n')
- self._win.attrset(0)
- for y, line in enumerate(lines):
- offset = 0
- # 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(
- y, offset,
- line.prepend + line.msg.txt[line.start_pos:line.end_pos])
- if y != self.height - 1:
- self.addstr('\n')
- self._win.attrset(0)
- 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))
diff --git a/poezio/xhtml.py b/poezio/xhtml.py
index 899985ef..0b234c29 100644
--- a/poezio/xhtml.py
+++ b/poezio/xhtml.py
@@ -488,7 +488,7 @@ def convert_simple_to_full_colors(text: str) -> str:
a \x19n} formatted one.
"""
# TODO, have a single list of this. This is some sort of
- # duplicate from windows.format_chars
+ # duplicate from ui.consts.FORMAT_CHARS
mapping = str.maketrans({
'\x0E': '\x19b',
'\x0F': '\x19o',
diff --git a/setup.py b/setup.py
index 5002f921..a81c3ad8 100755
--- a/setup.py
+++ b/setup.py
@@ -116,7 +116,7 @@ setup(name="poezio",
'Programming Language :: Python :: 3 :: Only'],
keywords=['jabber', 'xmpp', 'client', 'chat', 'im', 'console'],
packages=['poezio', 'poezio.core', 'poezio.tabs', 'poezio.windows',
- 'poezio_plugins', 'poezio_themes'],
+ 'poezio.ui', 'poezio_plugins', 'poezio_themes'],
package_dir={'poezio': 'poezio',
'poezio_plugins': 'plugins',
'poezio_themes': 'data/themes'},
diff --git a/test/test_ui/test_funcs.py b/test/test_ui/test_funcs.py
new file mode 100644
index 00000000..0e61549c
--- /dev/null
+++ b/test/test_ui/test_funcs.py
@@ -0,0 +1,46 @@
+from poezio.ui.funcs import (
+ find_first_format_char,
+ parse_attrs,
+ truncate_nick,
+)
+
+
+def test_find_char_not_present():
+ assert find_first_format_char("toto") == -1
+
+
+def test_find_char():
+ assert find_first_format_char('a \x1A 1') == 2
+
+
+def test_truncate_nick():
+ assert truncate_nick("toto") == "toto"
+
+
+def test_truncate_nick_wrong_size():
+ assert truncate_nick("toto", -10) == "t…"
+
+
+def test_truncate_nick_too_long():
+ nick = "012345678901234567"
+ assert truncate_nick(nick) == nick[:10] + "…"
+
+
+def test_truncate_nick_no_nick():
+ assert truncate_nick('') == ''
+
+
+def test_parse_attrs():
+ text = "\x19o\x19u\x19b\x19i\x191}\x19o\x194}"
+ assert parse_attrs(text) == ['4}']
+
+
+def test_parse_attrs_broken_char():
+ text = "coucou\x19"
+ assert parse_attrs(text) == []
+
+
+def test_parse_attrs_previous():
+ text = "coucou"
+ previous = ['u']
+ assert parse_attrs(text, previous=previous) == previous
diff --git a/test/test_ui/test_render.py b/test/test_ui/test_render.py
new file mode 100644
index 00000000..e0db5a8f
--- /dev/null
+++ b/test/test_ui/test_render.py
@@ -0,0 +1,145 @@
+import pytest
+from contextlib import contextmanager
+from datetime import datetime
+from poezio.theming import get_theme
+from poezio.ui.render import build_lines, Line, write_pre
+from poezio.ui.consts import SHORT_FORMAT
+from poezio.ui.types import BaseMessage, Message, StatusMessage, XMLLog
+
+def test_simple_build_basemsg():
+ msg = BaseMessage(txt='coucou')
+ line = build_lines(msg, 100, True, 10)[0]
+ assert (line.start_pos, line.end_pos) == (0, 6)
+
+
+def test_simple_render_message():
+ msg = Message(txt='coucou', nickname='toto')
+ line = build_lines(msg, 100, True, 10)[0]
+ assert (line.start_pos, line.end_pos) == (0, 6)
+
+
+def test_simple_render_xmllog():
+ msg = XMLLog(txt='coucou', incoming=True)
+ line = build_lines(msg, 100, True, 10)[0]
+ assert (line.start_pos, line.end_pos) == (0, 6)
+
+
+def test_simple_render_separator():
+ line = build_lines(None, 100, True, 10)[0]
+ assert line is None
+
+
+def test_simple_render_status():
+ class Obj:
+ name = 'toto'
+ msg = StatusMessage("Coucou {name}", {'name': lambda: Obj.name})
+ assert msg.txt == "Coucou toto"
+ Obj.name = 'titi'
+ build_lines(msg, 100, True, 10)[0]
+ assert msg.txt == "Coucou titi"
+
+
+class FakeBuffer:
+ def __init__(self):
+ self.text = ''
+
+ @contextmanager
+ def colored_text(self, *args, **kwargs):
+ yield None
+
+ def addstr(self, txt):
+ self.text += txt
+
+@pytest.fixture(scope='function')
+def buffer():
+ return FakeBuffer()
+
+@pytest.fixture
+def time():
+ return datetime.strptime('2019-09-27 10:11:12', '%Y-%m-%d %H:%M:%S')
+
+def test_write_pre_basemsg(buffer):
+ str_time = '10:11:12'
+ time = datetime.strptime(str_time, '%H:%M:%S')
+ msg = BaseMessage(txt='coucou', time=time)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '10:11:12 '
+ assert size == len(buffer.text)
+
+def test_write_pre_message_simple(buffer, time):
+ msg = Message(txt='coucou', nickname='toto', time=time)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '10:11:12 toto> '
+ assert size == len(buffer.text)
+
+
+def test_write_pre_message_simple_history(buffer, time):
+ msg = Message(txt='coucou', nickname='toto', time=time, history=True)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '2019-09-27 10:11:12 toto> '
+ assert size == len(buffer.text)
+
+
+def test_write_pre_message_highlight(buffer, time):
+ msg = Message(txt='coucou', nickname='toto', time=time, highlight=True)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '10:11:12 toto> '
+ assert size == len(buffer.text)
+
+def test_write_pre_message_no_timestamp(buffer):
+ msg = Message(txt='coucou', nickname='toto')
+ size = write_pre(msg, buffer, False, 10)
+ assert buffer.text == 'toto> '
+ assert size == len(buffer.text)
+
+
+def test_write_pre_message_me(buffer, time):
+ msg = Message(txt='/me coucou', nickname='toto', time=time)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '10:11:12 * toto '
+ assert size == len(buffer.text)
+
+
+def test_write_pre_message_revisions(buffer, time):
+ msg = Message(txt='coucou', nickname='toto', time=time, revisions=5)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '10:11:12 toto5> '
+ assert size == len(buffer.text)
+
+def test_write_pre_message_revisions_me(buffer, time):
+ msg = Message(txt='/me coucou', nickname='toto', time=time, revisions=5)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '10:11:12 * toto5 '
+ assert size == len(buffer.text)
+
+
+def test_write_pre_message_ack(buffer, time):
+ ack = get_theme().CHAR_ACK_RECEIVED
+ expected = '10:11:12 %s toto> ' % ack
+ msg = Message(txt='coucou', nickname='toto', time=time, ack=1)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == expected
+ assert size == len(buffer.text)
+
+
+def test_write_pre_message_nack(buffer, time):
+ nack = get_theme().CHAR_NACK
+ expected = '10:11:12 %s toto> ' % nack
+ msg = Message(txt='coucou', nickname='toto', time=time, ack=-1)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == expected
+ assert size == len(buffer.text)
+
+
+def test_write_pre_xmllog_in(buffer):
+ msg = XMLLog(txt="coucou", incoming=True)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '%s IN ' % msg.time.strftime('%H:%M:%S')
+ assert size == len(buffer.text)
+
+
+def test_write_pre_xmllog_out(buffer):
+ msg = XMLLog(txt="coucou", incoming=False)
+ size = write_pre(msg, buffer, True, 10)
+ assert buffer.text == '%s OUT ' % msg.time.strftime('%H:%M:%S')
+ assert size == len(buffer.text)
diff --git a/test/test_ui/test_types.py b/test/test_ui/test_types.py
new file mode 100644
index 00000000..e4c6c010
--- /dev/null
+++ b/test/test_ui/test_types.py
@@ -0,0 +1,126 @@
+import pytest
+from datetime import datetime
+
+from poezio.ui.types import BaseMessage, Message, XMLLog
+
+
+def test_create_message():
+ now = datetime.now()
+ msg = Message(
+ txt="coucou",
+ nickname="toto",
+ )
+ assert now < msg.time < datetime.now()
+
+ msg = Message(
+ txt="coucou",
+ nickname="toto",
+ time=now,
+ )
+ assert msg.time == now
+
+
+def test_message_offset_simple():
+ msg = Message(
+ txt="coucou",
+ nickname="toto",
+ )
+ example = "10:10:10 toto> "
+ assert msg.compute_offset(True, 10) == len(example)
+
+ msg = Message(
+ txt="coucou",
+ nickname="toto",
+ history=True,
+ )
+ example = "2019:09:01 10:10:10 toto> "
+ assert msg.compute_offset(True, 10) == len(example)
+
+def test_message_offset_no_nick():
+ msg = Message(
+ txt="coucou",
+ nickname="",
+ )
+ example = "10:10:10 "
+ assert msg.compute_offset(True, 10) == len(example)
+
+def test_message_offset_ack():
+ msg = Message(
+ txt="coucou",
+ nickname="toto",
+ ack=1,
+ )
+ example = "10:10:10 V toto> "
+ assert msg.compute_offset(True, 10) == len(example)
+
+ msg = Message(
+ txt="coucou",
+ nickname="toto",
+ ack=-1,
+ )
+ example = "10:10:10 X toto> "
+ assert msg.compute_offset(True, 10) == len(example)
+
+
+def test_message_offset_me():
+ msg = Message(
+ txt="/me coucou",
+ nickname="toto",
+ )
+ example = "10:10:10 * toto "
+ assert msg.compute_offset(True, 10) == len(example)
+
+
+def test_message_offset_revisions():
+ msg = Message(
+ txt="coucou",
+ nickname="toto",
+ revisions=3,
+ )
+ example = "10:10:10 toto3> "
+ assert msg.compute_offset(True, 10) == len(example)
+
+ msg = Message(
+ txt="coucou",
+ nickname="toto",
+ revisions=250,
+ )
+ example = "10:10:10 toto250> "
+ assert msg.compute_offset(True, 10) == len(example)
+
+
+def test_message_repr_works():
+ msg1 = Message(
+ txt="coucou",
+ nickname="toto",
+ revisions=250,
+ )
+ msg2 = Message(
+ txt="coucou",
+ nickname="toto",
+ old_message=msg1
+ )
+
+ assert repr(msg2) is not None
+
+def test_xmllog_offset():
+ msg = XMLLog(
+ txt='toto',
+ incoming=True,
+ )
+ example = '10:10:10 IN '
+ assert msg.compute_offset(True, 10) == len(example)
+
+ msg = XMLLog(
+ txt='toto',
+ incoming=False,
+ )
+ example = '10:10:10 OUT '
+ assert msg.compute_offset(True, 10) == len(example)
+
+def test_basemessage_offset():
+ msg = BaseMessage(
+ txt='coucou',
+ )
+ example = '10:10:10 '
+ assert msg.compute_offset(True, 10) == len(example)